Skip to main content

Lambda Expressions in Java: Syntax, Method References, Capture and How They Compile

Lambda expressions in Java explained with a runnable program: syntax forms, target typing, the four method-reference kinds, variable capture, this, and what javac actually generates.

A lambda expression is a short anonymous function: parameters, an arrow, and a body. It can be used wherever Java expects a functional interface, meaning an interface with exactly one abstract method. The compiler works out which interface from context (the target type), so the same text s -> s.toUpperCase() can be a Function in one place and your own interface in another. Lambdas arrived in Java 8 and are the basis of the Streams API and of most modern Java APIs.
FormExampleWhen
Explicit types(int x, int y) -> x + yRarely needed
Inferred types(x, y) -> x * yThe normal form
Block body(x, y) -> { int s = x - y; return s; }More than one statement
One parameterx -> x * xNo parentheses needed
No parameters() -> "no args"Suppliers and Runnables
s -> s.toUpperCase() target: Function<String,String> target: Shout (own interface) instance ofthat interface
Tested against. Temurin 25.0.4.1. Everything below comes from one program, not in a repository: download LambdaInDepth.java, then javac -g LambdaInDepth.java && java LambdaInDepth. The two compile errors come from CaptureError.java and NoTargetType.java. For the interfaces lambdas target, see the functional interfaces guide.

Syntax forms in practice

Calc a = (int x, int y) -> x + y;             // explicit types
Calc b = (x, y) -> x * y;                     // inferred types
Calc c = (x, y) -> { int s = x - y; return s; };   // block body needs return
Function<Integer, Integer> d = x -> x * x;
Supplier<String> e = () -> "no args";
5 6 -1 16 no args

A block body needs an explicit return when the interface method returns a value; an expression body returns its value implicitly. Download: LambdaInDepth.java.

Target typing: the lambda has no type of its own

A lambda expression is not an object of some built-in “lambda type”. Its type is whichever functional interface the context demands. Two identical bodies given two different targets produce unrelated types:

Function<String, String> f1 = s -> s.toUpperCase();
Shout f2 = s -> s.toUpperCase();
ABC ABC   <- same body; f1 and f2 are the same type: false

Source: LambdaInDepth.java. If there is no functional interface to target, the code does not compile; see the error section below.

The four method-reference kinds

A method reference is a lambda that only calls an existing method. There are four kinds and they behave differently:

KindExampleEquivalent lambda
StaticInteger::parseInts -> Integer.parseInt(s)
Bound instanceprefix::startsWithx -> prefix.startsWith(x), receiver fixed now
Unbound instanceString::lengths -> s.length(), first argument is the receiver
ConstructorArrayList::new() -> new ArrayList<>()
42 true 5 []

The output is, in order: parseInt("42"), "ab".startsWith("a"), "hello".length() and a new empty list. For where these show up in practice, see the collectors cookbook.

Going deeper: lambda or method reference?

Prefer a method reference when it reads more clearly than the lambda, and a lambda when the body does more than forward its arguments. A bound reference evaluates its receiver once, at creation time, so prefix::startsWith keeps using the original prefix even if the variable were reassigned (which the compiler forbids for locals anyway).

Capturing variables

A lambda can read local variables from the enclosing method, but only if they are final or effectively final (never reassigned). The reason: the lambda gets a copy of the value, and a copy that the original could later diverge from would be a bug waiting to happen.

int base = 10;
Supplier<Integer> cap = () -> base + 1;
int[] box = { 0 };
Runnable inc = () -> box[0]++;
inc.run(); inc.run();
captured base=10, result 11
array element workaround: 2  (the reference is final, the contents are not)

Source: LambdaInDepth.java. The one-element array works because the array reference never changes, only its contents. Treat it as a trick to understand, not a pattern to copy: for counters, prefer an AtomicInteger or a stream reduction. Trying to modify the captured local itself is a compile error, shown below.

Identity: do two evaluations give the same object?

A lambda that captures nothing needs no per-call state, so the JVM can create one instance and reuse it. A capturing lambda must carry its captured values, so each evaluation produces a new object.

non-capturing same instance : true
capturing    same instance : false

Source: LambdaInDepth.java. This is an implementation behaviour, not a language guarantee, so never compare lambdas with == or use them as keys. It also explains why non-capturing lambdas are very cheap.

this means the enclosing class

In an anonymous class, this is the anonymous object. In a lambda, this is the surrounding object, because a lambda is not a new scope in that sense.

lambda this.tag = outer, anonymous this.tag = anon
lambda is a synthetic class: true, anonymous: false

Source: LambdaInDepth.java. The second line is also telling: a lambda’s class is generated at run time and marked synthetic, whereas an anonymous class is a real class in your code base. A lambda can also not declare a local with the same name as a variable in the enclosing scope.

What javac generates

Unlike an anonymous class, a lambda does not produce a .class file of its own. The compiler moves the body into a private synthetic method in the same class, and the JVM links a call site to it the first time it runs (via invokedynamic). The class files for this program:

$ ls build/LambdaInDepth        (class files javac produced)
LambdaInDepth$1.class
LambdaInDepth$Calc.class
LambdaInDepth$Shout.class
LambdaInDepth.class

Built from LambdaInDepth.java. Only the anonymous class (LambdaInDepth$1) and the two interfaces appear. The lambda bodies are in the main class as private methods:

$ javap -p LambdaInDepth | grep 'lambda$'     (the lambda bodies are private synthetic methods)
  private java.lang.String lambda$thisDemo$0();
  private static java.lang.Integer lambda$makeCapturing$0(int);
  private static java.lang.String lambda$make$0();
  private static void lambda$main$8(int[]);
  private static java.lang.Integer lambda$main$7(int);
  private static java.lang.String lambda$main$6(java.lang.String);
  private static java.lang.String lambda$main$5(java.lang.String);
  private static java.lang.String lambda$main$4();
  private static java.lang.Integer lambda$main$3(java.lang.Integer);
  private static int lambda$main$2(int, int);
  private static int lambda$main$1(int, int);
  private static int lambda$main$0(int, int);

Notice that lambda$thisDemo$0 is an instance method (it uses this) while the others are static, and lambda$main$8(int[]) takes the captured array as a parameter. That is capture in the bytecode: captured values become extra arguments. Download: LambdaInDepth.java.

The main practical consequence is that stack traces show frames named like lambda$main$3, which is why it is worth keeping lambdas short.

The two compile errors you will meet

Changing a captured local, even after the lambda is created, breaks the effectively final rule. Download CaptureError.java:

lambda/CaptureError.java:7: error: local variables referenced from a lambda expression must be final or effectively final
        Supplier<Integer> s = () -> counter;
                                    ^
1 error

Assigning a lambda to a type that is not a functional interface fails too. Download NoTargetType.java:

lambda/NoTargetType.java:4: error: incompatible types: Object is not a functional interface
        Object o = () -> "no functional interface here";
                   ^
1 error

The fix for the second is to give the lambda a target, for example Runnable r = () -> ... or Supplier<String>.

Common mistakes

Modifying a captured local

It does not compile. Use a field, an atomic type, or restructure with a stream operation such as reduce.

Assuming this refers to the lambda

It refers to the enclosing instance. If you need an object with its own state and its own this, use an anonymous or named class.

Long lambda bodies

Past three or four lines, extract a method and pass a method reference. It reads better and gives a meaningful name in stack traces.

Comparing lambdas with ==

Reuse of non-capturing lambdas is an implementation detail, as the identity output shows. Do not depend on it.

Side effects inside stream lambdas

Mutating shared state from a parallel stream causes races. Keep lambdas pure; see the stream coding interview questions.

Common interview questions

What is a functional interface?

An interface with exactly one abstract method; default, static and Object methods do not count. @FunctionalInterface makes the compiler enforce it. See default methods for what else interfaces can hold.

Lambda versus anonymous class: what are the differences?

Meaning of this; a lambda compiles to a synthetic method and links through invokedynamic while an anonymous class gets its own class file; a lambda needs a functional interface while an anonymous class can extend any class or implement any interface.

Why must captured variables be effectively final?

The lambda holds a copy of the value, and the lambda may run later or on another thread. Forbidding reassignment keeps the copy and the original in agreement.

Name the four kinds of method reference.

Static (Integer::parseInt), bound instance (prefix::startsWith), unbound instance (String::length) and constructor (ArrayList::new).

FAQ

Since which version does Java support lambda expressions?

Java 8. See the Java 8 features guide.

Are lambdas faster than anonymous classes?

Usually equal or better after warm-up, and non-capturing lambdas allocate nothing per use. Do not choose between them on speed; measure if it matters.

Can a lambda throw a checked exception?

Only if the functional interface’s method declares it. Callable allows Exception; Runnable and Function do not.

Should you use a lambda? Yes for short, stateless behaviour passed to an API: comparators, predicates, callbacks. Use a named method or a class when the logic is long or needs its own state. Related: Optional, Comparable vs Comparator and how HashMap works.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.