A method is a named block of code that does one job: calculate a bill, validate a meter reading, format a ticket number. Splitting a program into methods keeps each part small and testable, as described under functions and abstraction. In Java every method lives inside a class, which is why the examples on this page are ordinary classes with a public static void main(String[] args) method and System.out.println, the style you will meet in most existing code (see your first program).

Defining and calling methods

A method declaration has a fixed order: optional modifiers such as static or public, the return type, the name, the parameters in parentheses (each with its type), and the body in braces. Method names are written in camelCase and usually start with a verb.

public class Billing {
    public static void main(String[] args) {
        double bill = costOf(350, 0.30);
        System.out.println("Bill: EUR " + bill);
        printHeader("INC-10042");
    }

    static double costOf(int kwh, double pricePerKwh) {
        return kwh * pricePerKwh;
    }

    static void printHeader(String ticket) {
        System.out.println("=== " + ticket + " ===");
    }
}
Bill: EUR 105.0
=== INC-10042 ===

The order of the methods in the class does not matter: main may call costOf although it is declared further down. The parameters kwh and pricePerKwh are local variables that receive the arguments of the call, 350 and 0.30, in order. An argument must fit the parameter's type: costOf("350", 0.30) does not compile. Money is calculated with double here only to keep the example short; real billing code uses BigDecimal (see variables, types and operators).

return and void

return value; ends the method and hands the value back to the caller. The value must match the declared return type. A method that returns nothing has the return type void; it may use a bare return; to stop early. A method with a real return type must return a value on every path through its body, and the compiler checks that:

public class Billing {
    public static void main(String[] args) {
        System.out.println(usageBand(450));
    }

    static String usageBand(int kwh) {
        if (kwh > 1000) {
            return "high";
        } else if (kwh > 300) {
            return "normal";
        }
    }
}
Main.java:12: error: missing return statement

For a reading of 300 kWh or less, nothing would be returned. Add a final return "low";, or throw an exception for input that should never occur. Calling a method and ignoring its return value is allowed, so costOf(350, 0.30); on its own line compiles but does nothing useful.

The method signature

A method signature in Java is the method's name together with the number, types and order of its parameters, such as costOf(int, double). Within one class, every signature must be unique. The return type is not part of the signature, so two methods that differ only in their return type clash:

public class Billing {
    public static void main(String[] args) {
        System.out.println(costOf(350, 0.30));
    }

    static double costOf(int kwh, double pricePerKwh) {
        return kwh * pricePerKwh;
    }

    static long costOf(int kwh, double pricePerKwh) {
        return Math.round(kwh * pricePerKwh);
    }
}
Main.java:10: error: method costOf(int,double) is already defined in class Billing

The reason is the call site: costOf(350, 0.30) gives the compiler no way to tell which of the two you mean. Parameter names do not count either; only their types do. The signature is also what the general idea of a signature as a contract refers to.

Overloading

Different signatures may share a name. This is method overloading, the same idea as in C#: the compiler picks the version whose parameters fit the arguments. System.out.println itself is overloaded, with versions for int, double, String, Object and more.

public class Labels {
    public static void main(String[] args) {
        System.out.println(describe(412));
        System.out.println(describe(412.75));
        System.out.println(describe("E-001", 412));
    }

    static String describe(int kwh) {
        return kwh + " kWh";
    }

    static String describe(double kwh) {
        return String.format("%.1f kWh", kwh);
    }

    static String describe(String meterId, int kwh) {
        return meterId + ": " + describe(kwh);
    }
}
412 kWh
412.8 kWh
E-001: 412 kWh

Overloads instead of default arguments

Java has no default parameter values and no named arguments. The usual replacement is a short overload that calls the full version with the missing value filled in, so the default lives in one place.

public class Tickets {
    public static void main(String[] args) {
        System.out.println(openTicket("Portal login fails"));
        System.out.println(openTicket("Substation offline", "P1"));
    }

    static String openTicket(String summary) {
        return openTicket(summary, "P3"); // default priority
    }

    static String openTicket(String summary, String priority) {
        return "[" + priority + "] " + summary;
    }
}
[P3] Portal login fails
[P1] Substation offline

How Java picks an overload

When the argument types do not match any version exactly, the compiler searches in three phases and stops at the first phase that finds a match. First it allows only an exact match and widening conversions (such as int to long, or String to Object), then also autoboxing (such as int to Integer), and only then varargs (below). So an older, cheaper conversion beats a newer one:

public class Resolution {
    public static void main(String[] args) {
        store(1);   // int argument
        archive(1);  // int argument
    }

    static void store(long value)    { System.out.println("store(long)"); }
    static void store(Integer value) { System.out.println("store(Integer)"); }

    static void archive(Object value) { System.out.println("archive(Object)"); }
    static void archive(int... values) { System.out.println("archive(int...)"); }
}
store(long)
archive(Object)

store(1) chose long because widening is tried before boxing. archive(1) chose Object (boxing to Integer, which is an Object) because varargs come last. When two versions are equally good, the call is ambiguous and does not compile: with f(int, double) and f(double, int), the call f(1, 1) gives error: reference to f is ambiguous.

Varargs

A parameter declared with three dots, int... readings, accepts any number of arguments of that type, including none. Inside the method, such a varargs (variable-arity) parameter is an ordinary array. The general idea is called a variadic function.

public class Readings {
    public static void main(String[] args) {
        System.out.println(total());
        System.out.println(total(412, 388, 395));
        System.out.println(total(new int[] { 351, 298 }));
        System.out.println(summary("E-001", 412, 388));
    }

    static int total(int... readings) {
        int sum = 0;
        for (int r : readings) {
            sum += r;
        }
        return sum;
    }

    static String summary(String meterId, int... readings) {
        return meterId + ": " + readings.length + " readings, " + total(readings) + " kWh";
    }
}
0
1195
649
E-001: 2 readings, 800 kWh

A method can have only one varargs parameter, and it must be the last one; total(int... readings, String unit) gives error: varargs parameter must be the last parameter. You can pass an existing array instead of separate values, as in the third call. Familiar varargs methods in the JDK are String.format, List.of and Arrays.asList.

Java always passes by value

When you call a method, Java copies the value of each argument into the parameter. This is called pass by value, and in Java there is no other way of passing arguments. For a primitive such as an int, the copy is the number itself, so the method cannot change the caller's variable. For an object, the value in the variable is a reference to the object, and that reference is copied. Both variables then point to the same object.

public class Passing {
    public static void main(String[] args) {
        int kwh = 100;
        addEstimate(kwh);
        System.out.println("kwh after addEstimate: " + kwh);

        Customer customer = new Customer("Amara Okafor");
        rename(customer);
        System.out.println("customer after rename: " + customer.name);
    }

    static void addEstimate(int kwh) {
        kwh = kwh + 50; // changes the copy only
    }

    static void rename(Customer c) {
        c.name = "Amara Okafor-Mensah";   // changes the shared object
        c = new Customer("Somebody else"); // points the copy elsewhere
        c.name = "Not visible to main";
    }
}

class Customer {
    String name;

    Customer(String name) {
        this.name = name;
    }
}
kwh after addEstimate: 100
customer after rename: Amara Okafor-Mensah

Inside rename, the first line follows the copied reference to the shared object and changes it, which main sees. The second line only makes the local copy c point to a new object; main's variable still points to the old one. The Customer class is declared below Passing; classes are explained on classes and objects.

Two stack frames, main and rename, each hold a reference variable. Both point to the same Customer object on the heap; after the reassignment, rename's copy points to a second Customer object. Stack: one frame per call main customer rename c (a copy) Heap: objects Customer name = "Amara Okafor-Mensah" Customer name = "Not visible to main" Solid: before c = new Customer(…). Dashed: after it.
Both frames hold their own variable, but the values are the same reference. Changing the object is visible everywhere; pointing the copy elsewhere is not.

Arrays behave the same way, because an array is an object: a method that receives an array can change its elements, and the caller sees the change. The underlying model of stack frames and heap objects is explained under values and references, and the general terms under passing arguments.

Returning several values

Because a method cannot write into the caller's variables, it returns everything it produces. When that is more than one value, return a small record rather than an array of mixed things or a modified parameter.

public class Stats {
    public static void main(String[] args) {
        UsageRange range = rangeOf(412, 388, 395, 351);
        System.out.println(range);
        System.out.println("Spread: " + (range.max() - range.min()) + " kWh");
    }

    static UsageRange rangeOf(int... readings) {
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for (int r : readings) {
            min = Math.min(min, r);
            max = Math.max(max, r);
        }
        return new UsageRange(min, max);
    }
}

record UsageRange(int min, int max) {}
UsageRange[min=351, max=412]
Spread: 61 kWh

Static and instance methods

The keyword static marks a member that belongs to the class itself instead of to individual objects. A static method is called on the class (Math.max(3, 7), Billing.costOf(…), or just costOf(…) inside the same class) and has no object to work with. An instance method, declared without static, is called on an object (name.length()) and can use that object's fields through this.

Because main is static in a classic program, it cannot call an instance method of its own class directly; there is no object yet:

public class Report {
    public static void main(String[] args) {
        printTitle();
    }

    void printTitle() {
        System.out.println("Monthly usage report");
    }
}
Main.java:3: error: non-static method printTitle() cannot be referenced from a static context

Either make printTitle static, or create an object and call the method on it. Which one is right depends on whether the method needs object state. Utility calculations that only use their parameters are usually static; behavior that uses an object's fields is an instance method.

public class Report {
    private final String month;

    Report(String month) {
        this.month = month;
    }

    public static void main(String[] args) {
        Report report = new Report("September");
        report.printTitle();
        System.out.println(percent(395, 412) + "% of last month");
    }

    void printTitle() {
        System.out.println("Usage report for " + month);
    }

    static long percent(int part, int whole) {
        return Math.round(100.0 * part / whole);
    }
}
Usage report for September
96% of last month

Recursion

A method that calls itself is recursive. Recursion fits data that is itself nested, such as a grid with substations that feed smaller stations. Each call gets its own stack frame with its own parameters, and a base case that does not call the method again makes sure the calls end.

import java.util.List;

public class Grid {
    public static void main(String[] args) {
        Station grid = new Station("Delft-Noord", 120, List.of(
                new Station("Tanthof", 340, List.of()),
                new Station("Voorhof", 210, List.of(
                        new Station("Buitenhof", 95, List.of())))));

        System.out.println("Total load: " + totalLoad(grid) + " kW");
    }

    static int totalLoad(Station station) {
        int total = station.ownLoad();
        for (Station child : station.feeds()) {
            total += totalLoad(child); // recursive call
        }
        return total; // base case: a station without children
    }
}

record Station(String name, int ownLoad, List<Station> feeds) {}
Total load: 765 kW

The import java.util.List; line is needed in an ordinary class; compact source files import the whole java.base module automatically. Without a base case, or with one that is never reached, the calls pile up until the thread's stack is full and the JVM gives up:

public class Countdown {
    public static void main(String[] args) {
        System.out.println(daysLeft(10));
    }

    static int daysLeft(int days) {
        return daysLeft(days - 1); // no base case
    }
}
Exception in thread "main" java.lang.StackOverflowError

StackOverflowError is an Error, not an ordinary exception: it signals a problem the program normally cannot recover from, and you should fix the code instead of catching it (see exceptions and error handling). Java does not optimize tail calls, so very deep recursion (many thousands of levels) can overflow even when it is correct. For long linear sequences, use a loop.