Lambdas and Method References

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

A lambda expression is an unnamed function used as a value, written wherever an instance of a functional interface (an interface with a single abstract method) is expected. This page follows dev.java "Lambda Expressions" and the Java Tutorials "Lambda Expressions" lesson.

Lambda Syntax and Target Typing

A lambda has a parameter list, an arrow ->, and a body that is either one expression or a brace-enclosed block of statements.

import java.util.function.*;

Supplier<String> greet = () -> "hello";                        // no parameters
Function<Integer, Integer> square = n -> n * n;                // one parameter, expression body
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;   // two parameters
Predicate<String> isBlank = s -> s.strip().isEmpty();

Runnable task = () -> {                                        // block body, no value produced
    System.out.println("working");
    System.out.println("done");
};

Function<Integer, String> classify = n -> {                    // block body, explicit return
    if (n < 0) {
        return "negative";
    }
    return n == 0 ? "zero" : "positive";
};

A lambda has no type of its own. The compiler reads the target type from context — the variable being assigned, the parameter being passed, the value being returned — and checks the lambda against that interface’s single abstract method. The very same (a, b) -> a + b is a BiFunction, an IntBinaryOperator, or a custom interface depending on where it appears. With no functional-interface target it does not compile:

Object o = () -> "nope";        // does NOT compile: Object is not a functional interface

The java.util.function package supplies the common shapes (Function, Predicate, Supplier, Consumer, BiFunction, and the primitive specialisations); prefer them over hand-rolled interfaces.

Parameter types and var

Parameter types are normally inferred and left off. You may write them explicitly, or use var for every parameter (all-or-nothing) so an annotation can be attached:

BiFunction<Integer, Integer, Integer> a = (Integer x, Integer y) -> x + y;   // explicit types
BiFunction<Integer, Integer, Integer> b = (x, y) -> x + y;                    // inferred
BiFunction<Integer, Integer, Integer> c = (var x, var y) -> x + y;           // var form

You cannot mix the forms: (var x, y) and (var x, Integer y) are both errors. A single inferred parameter may drop its parentheses (n -> n * n); the var and explicit forms always keep them. The grammar is defined in JLS 15.27.

Capturing Variables and this

A lambda may use local variables from the enclosing scope only if they are effectively final — assigned exactly once. That lets the captured value be copied safely.

import java.util.function.Function;

static Function<String, String> prefixer(String prefix) {
    int calls = 0;
    // return s -> { calls++; return prefix + s; };   // does NOT compile: calls is reassigned
    return s -> prefix + s;                            // OK: prefix is effectively final
}

To accumulate state across calls, capture a reference to a mutable holder — an array, an AtomicLong, a field — rather than reassigning a local:

var count = new java.util.concurrent.atomic.AtomicLong();
Runnable tick = () -> count.incrementAndGet();   // the reference is final; the object mutates
tick.run();
tick.run();
System.out.println(count.get());   // 2

Inside a lambda, this refers to the enclosing instance — the object whose method is running — just as it would in the surrounding code. A lambda opens no new scope: its parameters and locals share the enclosing method’s namespace and cannot shadow a name already in scope.

class Widget {
    private String name = "widget";

    Runnable namePrinter() {
        return () -> System.out.println(this.name);   // this == the Widget; prints "widget"
    }
}

Method References

When a lambda does nothing but call one existing method, a method reference written with :: names that method directly. See Method References. There are four kinds:

Kind Syntax Equivalent lambda Example

Static method

Type::staticMethod

a -> Type.staticMethod(a)

Integer::parseInt

Bound instance method

object::method

a -> object.method(a)

System.out::println

Unbound instance method

Type::instanceMethod

(obj, a) -> obj.instanceMethod(a)

String::toUpperCase

Constructor

Type::new

a -> new Type(a)

ArrayList::new

import java.util.*;
import java.util.function.Supplier;
import java.util.stream.*;

List<String> words = List.of("gamma", "alpha", "beta");

words.stream().map(String::toUpperCase).forEach(System.out::println);   // unbound, then bound

List<Integer> lengths = words.stream().map(String::length).toList();

int total = Stream.of("1", "2", "3").mapToInt(Integer::parseInt).sum();   // static; total == 6

Supplier<List<String>> freshList = ArrayList::new;                        // constructor reference
List<String> sorted = words.stream()
        .collect(Collectors.toCollection(TreeSet::new))
        .stream()
        .toList();

The unbound form String::toUpperCase takes the receiver as its first argument, so it matches a Function<String, String>; String::compareTo matches a Comparator<String>.

Lambda vs. Anonymous Class

A lambda is not merely terser syntax for an anonymous class that implements a functional interface. Three concrete differences:

  • No new scope. A lambda body sees the enclosing method’s locals directly and cannot re-declare a name already visible. An anonymous class opens a fresh scope and may shadow.

  • this. In a lambda, this is the enclosing instance. In an anonymous class, this is the anonymous object; the enclosing instance is Enclosing.this.

  • No separate class file. The compiler renders a lambda through an invokedynamic call site rather than a synthetic Widget$1.class, and may reuse a single instance for a stateless lambda. An anonymous class always produces its own class file and a fresh object per evaluation.

int factor = 3;

Function<Integer, Integer> viaLambda = n -> n * factor;       // sees the enclosing 'factor'

Function<Integer, Integer> viaAnon = new Function<>() {
    @Override
    public Integer apply(Integer n) {
        int factor = 10;             // legal: the anonymous class has its own scope and shadows
        return n * factor;
    }
};

Use a lambda for a one-method behaviour with no state of its own; use an anonymous class when you need multiple methods, instance fields, or to extend a class — see Nested and Anonymous Classes.

See Also