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.
| Form | Example | When |
|---|---|---|
| Explicit types | (int x, int y) -> x + y | Rarely needed |
| Inferred types | (x, y) -> x * y | The normal form |
| Block body | (x, y) -> { int s = x - y; return s; } | More than one statement |
| One parameter | x -> x * x | No parentheses needed |
| No parameters | () -> "no args" | Suppliers and Runnables |
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:
| Kind | Example | Equivalent lambda |
|---|---|---|
| Static | Integer::parseInt | s -> Integer.parseInt(s) |
| Bound instance | prefix::startsWith | x -> prefix.startsWith(x), receiver fixed now |
| Unbound instance | String::length | s -> s.length(), first argument is the receiver |
| Constructor | ArrayList::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.
No Comments yet!