Polyglot
Languages Java functions
Java § functions

Methods and lambdas

Methods are the principal callable construct in Java: free functions are not admitted (every method belongs to a class or interface). Static methods on utility classes serve the role free functions play in C++. The language additionally provides lambdas (since Java 8) — anonymous function expressions — and method references — values that designate a method without invoking it. Together with functional interfaces (interfaces with exactly one abstract method), the combination is Java’s substitute for first-class functions and the substrate on which streams, the concurrency utilities, and modern functional-style code are built.

This page covers method declarations, parameters, lambdas, method references, functional interfaces, and the conventions for using each. The deeper interface mechanics are in Interfaces; the streams API is in Streams.

Method declaration

A method declaration combines an access modifier, optional static, optional other modifiers, a return type, a name, a parameter list, optional throws clause, and a body:

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int value() {
        return count;
    }

    public static int compare(Counter a, Counter b) {
        return Integer.compare(a.count, b.count);
    }

    public void load(File file) throws IOException {
        // may throw IOException; declared in the throws clause
    }
}

The full treatment of access modifiers is in Packages. The principal forms of method declaration are familiar from C++ and C#; the Java-specific aspects are documented below.

Parameters

Java methods support several parameter forms.

Positional parameters

The conventional form:

public void greet(String name, int age) {
    System.out.printf("Hello, %s, you are %d years old%n", name, age);
}

greet("Alice", 30);

Varargs

A varargs parameter accepts any number of arguments:

public void log(String level, String... messages) {
    for (String msg : messages) {
        System.out.println("[" + level + "] " + msg);
    }
}

log("INFO");
log("WARN", "first message");
log("ERROR", "first", "second", "third");

The varargs are received as a String[] array. Inside the method, messages is a regular array; its length is the number of arguments passed.

The varargs parameter must be the last parameter; only one varargs parameter is admitted per method. Generic varargs are slightly tricky (see Generics and the @SafeVarargs annotation).

No default arguments

Java does not support default arguments. The conventional substitute is method overloading:

public void connect(String host) {
    connect(host, 80, 5000);
}

public void connect(String host, int port) {
    connect(host, port, 5000);
}

public void connect(String host, int port, int timeoutMs) {
    /* the actual implementation */
}

Or, for many parameters, a builder pattern:

public class ConnectionConfig {
    private String host;
    private int    port      = 80;
    private int    timeoutMs = 5000;

    public ConnectionConfig host(String h) { this.host = h; return this; }
    public ConnectionConfig port(int p) { this.port = p; return this; }
    public ConnectionConfig timeout(int t) { this.timeoutMs = t; return this; }

    public Connection build() { /* ... */ }
}

Connection c = new ConnectionConfig()
    .host("example.com")
    .timeout(10000)
    .build();

The builder pattern is the conventional Java idiom for constructors with many optional parameters.

Pass-by-value

Java is always pass-by-value. For primitive parameters, the value is copied. For reference parameters, the reference itself is copied — the parameter and the caller’s variable both designate the same object, so modifications to the object are visible to the caller, but reassigning the parameter is invisible.

The full discussion is in References and primitives.

Method overloading

Two or more methods in the same class may share a name as long as they differ in the signature — the parameter types or the number of parameters:

public void print(int n)       { /* ... */ }
public void print(double d)    { /* ... */ }
public void print(String s)    { /* ... */ }
public void print(int n, int width) { /* ... */ }

Overload resolution selects the best match. The standard defines an elaborate ranking — exact match, widening primitive conversion, autoboxing, varargs — that selects the candidate requiring the fewest conversions. Ambiguity is a compile-time error.

Methods cannot differ only in:

  • Their return type.
  • The presence of throws.
  • The names of parameters (only types matter).

Return type and throws

A method’s return type appears before the name. A method that returns nothing has return type void:

public int  add(int a, int b) { return a + b; }
public void announce(String message) { System.out.println(message); }

A method that may throw a checked exception must declare it in the throws clause:

public String readFirstLine(File file) throws IOException {
    try (var reader = Files.newBufferedReader(file.toPath())) {
        return reader.readLine();
    }
}

Checked exceptions are part of the method’s contract; callers must either catch them or re-declare. Unchecked exceptions (subtypes of RuntimeException or Error) need not appear in throws. The full treatment is in Error handling.

Static, instance, and final methods

ModifierEffect
(none)Instance method; receives the implicit this
staticClass method; no this; called via ClassName.method()
final(On instance method) cannot be overridden by subclasses
abstract(On instance method) has no body; the class must be abstract
synchronizedAcquires the instance/class monitor for the call duration
nativeImplemented in non-Java code (typically through JNI)

Static methods belong to the type, not to instances:

public class MathUtils {
    public static double square(double x) { return x * x; }
}

double n = MathUtils.square(5);

The conventional uses are utility methods, factory methods (List.of, Map.of), and entry points (main).

Lambda expressions

Java 8 introduced lambdas — anonymous function expressions:

Runnable          task   = () -> System.out.println("running");
Function<Integer, Integer> square = x -> x * x;
BinaryOperator<Integer>     add    = (a, b) -> a + b;
Predicate<String>           empty  = s -> s.isEmpty();

The lambda syntax has two forms:

  1. Expression body: params -> expression. Returns the value of the expression.
  2. Statement body: params -> { statements }. May contain multiple statements; uses return to produce a value.

The parameter list:

  • Single inferred parameter: x -> … (parentheses omittable).
  • Multiple or typed parameters: (int x, int y) -> … or (x, y) -> ….
  • No parameters: () -> ….

Java 11 admitted var parameters, primarily for adding annotations:

Function<Integer, Integer> abs = (var x) -> x < 0 ? -x : x;
Function<Integer, Integer> abs2 = (@Nonnull var x) -> x < 0 ? -x : x;

A lambda’s target type is determined by the context — the variable type, the parameter type, the return type. The target type must be a functional interface (treated below); a lambda cannot be assigned to a generic Object-typed variable.

Captures

A lambda may reference variables from the enclosing scope:

public Function<Integer, Integer> adder(int delta) {
    return x -> x + delta;     // captures `delta`
}

var add5 = adder(5);
int n    = add5.apply(10);     // 15

Captured variables must be effectively final — they may not be reassigned after the lambda is created (or before; the compiler tracks the entire scope). The restriction is the conventional defence against the JavaScript-style closure-over-mutating-loop-variable bug:

List<Runnable> tasks = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    int finalI = i;            // effectively final copy
    tasks.add(() -> System.out.println(finalI));
}

For mutable shared state, the conventional substitute is a one-element array (int[1]) or an AtomicInteger. The discipline catches more bugs than it produces.

Method references

A method reference is a value that designates a method without invoking it:

Function<String, Integer> length = String::length;     // instance method
Supplier<List<String>>    create = ArrayList::new;     // constructor reference
Consumer<String>          print  = System.out::println; // bound instance method
Function<Integer, Integer> abs   = Math::abs;          // static method

Method references are syntactic sugar for the corresponding lambda; String::length is equivalent to (String s) -> s.length(). The forms:

FormExampleEquivalent lambda
Static methodMath::absn -> Math.abs(n)
Bound instance methodobj::methodargs -> obj.method(args)
Unbound instance methodString::lengths -> s.length()
ConstructorArrayList::new() -> new ArrayList<>()

The conventional discipline:

  • Use a method reference when the lambda’s body is a single method call.
  • Use a lambda when the body is non-trivial.

Functional interfaces

A functional interface is an interface with exactly one abstract method (after default and static methods are excluded). Lambdas and method references are assignable to functional interfaces; the compiler verifies the signature match.

@FunctionalInterface
public interface IntOperation {
    int apply(int x);
}

IntOperation square = x -> x * x;
int n = square.apply(5);           // 25

The @FunctionalInterface annotation is optional but conventional; it informs the compiler to verify the single-abstract-method property.

The standard library provides a substantial set of functional interfaces in java.util.function:

InterfaceSignatureUse
Function<T, R>R apply(T)A function from T to R
BiFunction<T, U, R>R apply(T, U)A function of two arguments
Consumer<T>void accept(T)A consumer of T
BiConsumer<T, U>void accept(T, U)A consumer of two values
Supplier<T>T get()A producer of T
Predicate<T>boolean test(T)A boolean test on T
BiPredicate<T, U>boolean test(T, U)A boolean test of two values
UnaryOperator<T>T apply(T)A Function from T to T
BinaryOperator<T>T apply(T, T)A BiFunction from T,T to T
Runnablevoid run()A no-arg, no-result computation

Plus primitive specialisations:

InterfaceSignature
IntFunction<R>R apply(int)
IntPredicateboolean test(int)
IntUnaryOperatorint applyAsInt(int)
IntBinaryOperatorint applyAsInt(int, int)
IntConsumervoid accept(int)
IntSupplierint getAsInt()
ToIntFunction<T>int applyAsInt(T)

Similar specialisations exist for long and double. The primitive specialisations avoid boxing in numeric workloads.

The functional-interface mechanism is the foundation of streams, the forEach method, the comparator-based sorting, and most modern Java APIs that take a callback.

Local classes and anonymous classes

Java has supported local classes (declared inside a method) and anonymous classes (one-off class expressions) since Java 1.0. They are largely superseded by lambdas:

// Anonymous class (the older form):
Runnable task = new Runnable() {
    @Override
    public void run() { System.out.println("running"); }
};

// Lambda (Java 8+):
Runnable task = () -> System.out.println("running");

The lambda form is shorter, has cleaner semantics around this (the lambda’s this is the enclosing instance, not a new one), and is the conventional contemporary choice for one-method use cases.

Anonymous classes remain useful when:

  • The target interface has more than one abstract method.
  • The implementation needs to maintain instance fields.
  • A reference to the anonymous class itself is needed (rare).

Specifiers and annotations

Several specifiers refine method declarations:

SpecifierEffect
public / protected / privateAccess (or package-private if absent)
staticBelongs to the type, not to instances
finalCannot be overridden
abstractNo body; the class must be abstract
synchronizedAcquires the monitor for the call
nativeImplementation supplied externally
strictfpFloating-point semantics are platform-independent (mostly redundant since Java 17)
default(Interface only) provides an implementation in the interface

Plus annotations that affect tooling:

AnnotationEffect
@OverrideVerifies the method overrides a superclass or interface method
@DeprecatedMarks the method as deprecated; usage produces a diagnostic
@SuppressWarnings(...)Silences specified warnings
@FunctionalInterface(On the interface) verifies single-abstract-method property
@SafeVarargsSuppresses unchecked-warning for varargs of generic types

A note on what Java does not have

The features that some other languages provide and their status in Java:

FeatureAvailable?
First-class free functionsNo. Static methods on classes are the substitute.
Nested functionsYes (local classes, lambdas)
Function compositionThrough Function.andThen, Function.compose
Currying / partial applicationManual (lambdas that capture; common in functional libraries like Vavr)
Multiple return valuesThrough records, custom classes, or Map.Entry
Default argumentsNo. Use overloading or a builder.
Named argumentsNo.
VariadicYes (...)
Generic methodsYes
Operator overloadingNo (except + for String)
Default interface methodsYes (since Java 8)
Pattern matchingYes (since Java 16/21)

The combination — instance and static methods, lambdas, method references, functional interfaces, generics, default methods, and pattern matching — covers most of the practical method-based programming patterns. The lack of operator overloading and free functions are the most-cited limitations; the conventional Java response is that the resulting code is more uniform and easier to read.