Methods and Parameters
Writing methods in Java: parameters and return values, why Java always passes by value, overloading, varargs, static methods and recursion.
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.
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.