Java Module 9 – Lambdas, functional interfaces and Optional
Java lambda expressions and target typing
Write Java lambda expressions, see target typing give one lambda four types, fix effectively final errors and read what javac compiles a lambda into.
What you will learn
- Write lambda expressions for functional interfaces
- Explain target typing and effectively final capture
- Choose between a lambda, a method reference and a named method
What you’ll learn
What you will learn
- Write lambda expressions for functional interfaces
- Explain target typing and effectively final capture
- Choose between a lambda, a method reference and a named method
On this page
A lambda expression is a function written right where it is used: m -> m >= 40 takes a mark and says whether
it passes. In Java a lambda is never a standalone function, though. It is an object that implements a
functional interface, an interface with exactly one abstract method, such as Predicate<Integer> or
Comparator<Student>. The lambda never names that interface. The compiler takes it from the place where the lambda
appears, its target type, so the same text m -> m >= 40 is an IntPredicate on one line and a
Function<Integer, Boolean> on the next. Two more rules complete the picture: a lambda may use the local variables
of the method around it only if they never change after they get their value (they are effectively final), and
this inside a lambda means the object of the code around it. Every rule below is run on JDK 25, compiler errors
included, and the last section uses javap to show what javac turns a lambda into.
From an anonymous class to a lambda
Before Java 8, handing a piece of behaviour to a method meant writing a class. Sorting students by marks, highest
first, needed an anonymous class that implements Comparator<Student>. The lambda says the same thing in one line:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
record Student(String name, int marks) {
@Override
public String toString() { return name + " " + marks; }
}
class SortNames {
static void main() {
List<Student> students = List.of(new Student("Ravi", 82), new Student("Asha", 91),
new Student("Meera", 82), new Student("Kabir", 67));
// Before Java 8: an anonymous class that implements Comparator.
List<Student> first = new ArrayList<>(students);
first.sort(new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return Integer.compare(b.marks(), a.marks());
}
});
IO.println("anonymous class: " + first);
// The same comparator as a lambda expression.
List<Student> second = new ArrayList<>(students);
second.sort((a, b) -> Integer.compare(b.marks(), a.marks()));
IO.println("lambda: " + second);
}
} from session import run
run("""
java SortNames.java
javac -d classes SortNames.java
ls classes
""") Output
# javac 25.0.4.1 $ java SortNames.java anonymous class: [Asha 91, Ravi 82, Meera 82, Kabir 67] lambda: [Asha 91, Ravi 82, Meera 82, Kabir 67] $ javac -d classes SortNames.java $ ls classes SortNames.class SortNames$1.class Student.class
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sort_session.py
Both sorts give the same order. Ravi stays ahead of Meera on 82 marks because List.sort is stable: elements that
compare as equal keep their input order, which the comparators lesson of this module builds on. The listing of the
compiled classes shows a difference you cannot see in the output: the anonymous class became a file of its own,
SortNames$1.class, while the lambda added no class file at all. Where its code went is the subject of the last
section.
Read the lambda from left to right. (a, b) are its parameters, -> separates them from the body, and
Integer.compare(b.marks(), a.marks()) is the body, an expression whose value is the result. Nothing says that
a and b are Student objects or that the result is an int: both come from the one abstract method of the
target type, int compare(Student a, Student b).
The syntax, piece by piece
import java.util.Map;
import java.util.TreeMap;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.BinaryOperator;
import java.util.function.Function;
class LambdaForms {
static void main() {
Runnable hello = () -> IO.println("no parameters");
Function<String, Integer> length = s -> s.length(); // one parameter
BinaryOperator<Integer> add = (a, b) -> a + b; // two parameters
BiFunction<String, Integer, String> repeat = (String s, Integer n) -> s.repeat(n);
BiPredicate<String, String> same = (var x, var y) -> x.equalsIgnoreCase(y);
Function<Integer, String> grade = marks -> { // a block body
if (marks >= 90) return "A";
if (marks >= 75) return "B";
return "C";
};
hello.run();
IO.println(length.apply("target"));
IO.println(add.apply(40, 2));
IO.println(repeat.apply("ab", 3));
IO.println(same.test("Java", "JAVA"));
IO.println(grade.apply(82));
Map<String, Integer> marks = new TreeMap<>(Map.of("Asha", 91, "Ravi", 82));
marks.forEach((_, m) -> IO.println("a mark: " + m)); // _ : a parameter not used
}
} from session import run
run("""
java LambdaForms.java
""") Output
# javac 25.0.4.1 $ java LambdaForms.java no parameters 6 42 ababab true B a mark: 91 a mark: 82
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 forms_session.py
The program uses every form of the syntax once:
- No parameters:
() -> IO.println("no parameters"), the shape ofRunnableandSupplier. - One parameter:
s -> s.length(). Only here may the parentheses be left out. - Several parameters:
(a, b) -> a + b, with the types inferred from the target. - Declared types:
(String s, Integer n) -> s.repeat(n). Type all the parameters or none of them. var(Java 11):(var x, var y) -> x.equalsIgnoreCase(y), useful when a parameter needs an annotation.- A block body:
marks -> { if (…) return "A"; … }, for several statements; every value needs areturn. - Unnamed
_(Java 22):(_, m) -> IO.println("a mark: " + m), for a parameter the body does not use.
An expression body has no return and no semicolon: its value is the result. A block body is an ordinary method
body, so a lambda whose target method returns a value must return it on every path, and one whose target returns
void must not return a value. The forms cannot be mixed in one parameter list: (var x, y) and (String x, y) are both
compile errors. When a block body grows past a few lines, as grade already has, it reads better as a named method,
a choice the section on lambdas, method references and named methods comes back to.
Target typing: the context decides the type
A lambda has no type of its own. The Java Language Specification (§15.27) allows one only where the context supplies
a type: an assignment or variable declaration (a return statement counts, since it assigns the method’s result), an
argument of a method call, or a cast. The program below writes the same lambda four times and prints which
interface each resulting object implements:
import java.util.function.Function;
import java.util.function.IntPredicate;
import java.util.function.Predicate;
interface MarksRule {
boolean passes(int marks);
}
class TargetTyping {
static String typeOf(Object lambda) {
return lambda.getClass().getInterfaces()[0].getSimpleName();
}
static IntPredicate atLeast(int min) {
return m -> m >= min; // return context
}
static void report(String what, Predicate<String> rule) {
IO.println(what + " -> " + rule.test(" "));
}
static void main() {
// One lambda text, four target types (assignment contexts).
IntPredicate a = m -> m >= 40;
Predicate<Integer> b = m -> m >= 40;
Function<Integer, Boolean> c = m -> m >= 40;
MarksRule d = m -> m >= 40;
IO.println(typeOf(a) + ", " + typeOf(b) + ", " + typeOf(c) + ", " + typeOf(d));
IO.println(a.test(38) + " " + b.test(38) + " " + c.apply(38) + " " + d.passes(38));
report("blank?", s -> s.isBlank()); // invocation context
IO.println(atLeast(35).test(38));
Object task = (Runnable) () -> IO.println("cast context");
((Runnable) task).run();
}
} One lambda expression, four target types
Text description of the diagram
The diagram shows how the same lambda text gets its type from where it is written.
- At the top is one lambda expression: m -> m >= 40. It does not name any interface.
- In the middle are four declarations that each give it a target type. IntPredicate a, whose one abstract method is boolean test(int m). Predicate<Integer> b, whose method is boolean test(Integer m). Function<Integer, Boolean> c, whose method is Boolean apply(Integer m). MarksRule d, an interface written for the lesson, whose method is boolean passes(int m).
- The arrow from the lambda to the declarations says that javac checks the body against the one abstract method of the target interface: the parameter m is an int for IntPredicate and MarksRule and an Integer for the other two, and the result must fit the method's return type.
- At the bottom, evaluating the lambda produces an object of that interface. Calling its one method runs the lambda's body.
from session import run
run("""
java TargetTyping.java
java NoTarget.java
""", fails=("java NoTarget.java",)) Output
# javac 25.0.4.1
$ java TargetTyping.java
IntPredicate, Predicate, Function, MarksRule
false false false false
blank? -> true
true
cast context
$ java NoTarget.java
NoTarget.java:3: error: cannot infer type for local variable rule
var rule = m -> m >= 40;
^
(lambda expression needs an explicit target-type)
NoTarget.java:4: error: incompatible types: Object is not a functional interface
Object task = () -> IO.println("hello");
^
2 errors
error: compilation failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 target_session.py
The first line of output is the point of the section: one lambda text, four interfaces. The target also fixes the
types inside the lambda. For IntPredicate and MarksRule the parameter m is an int; for Predicate<Integer>
and Function<Integer, Boolean> it is an Integer, unboxed for the comparison. report(…) shows the most common
context of all, a method argument, and atLeast(35) a return statement.
NoTarget.java fails for two different reasons. var takes a variable’s type from its initial value, and a lambda
has none to give, so javac asks for an explicit target type. Object is not a functional interface, because it
has no abstract method for the lambda to implement. Both are fixed by naming an interface: declare
IntPredicate rule = …, or cast as TargetTyping does with (Runnable) () -> ….
Capturing variables: the effectively final rule
A lambda may read local variables of the method it is written in. That is called capturing them, and there is a limit. Here a lambda tries to count the failed marks:
import java.util.List;
class CountFailures {
static void main() {
List<Integer> marks = List.of(82, 38, 91, 25, 67);
int failed = 0;
marks.forEach(m -> {
if (m < 40) failed++;
});
IO.println(failed + " failed");
}
} import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
class CountFixed {
static void main() {
List<Integer> marks = List.of(82, 38, 91, 25, 67);
// 1. A plain loop: no lambda, so the local variable may change.
int failed = 0;
for (int m : marks) {
if (m < 40) failed++;
}
IO.println("loop: " + failed + " failed");
// 2. Let the stream count, so there is nothing to change.
long count = marks.stream().filter(m -> m < 40).count();
IO.println("stream: " + count + " failed");
// 3. The variable never changes; the object it points to does.
AtomicInteger counter = new AtomicInteger();
marks.forEach(m -> {
if (m < 40) counter.incrementAndGet();
});
IO.println("AtomicInteger: " + counter.get() + " failed");
// Effectively final: assigned once and never changed, so a lambda may read it.
int passMark = 40;
IO.println("passed: " + marks.stream().filter(m -> m >= passMark).count());
}
} from session import run
run("""
java CountFailures.java
java CountFixed.java
""", fails=("java CountFailures.java",)) Output
# javac 25.0.4.1
$ java CountFailures.java
CountFailures.java:8: error: local variables referenced from a lambda expression must be final or effectively final
if (m < 40) failed++;
^
1 error
error: compilation failed
[exit status 1]
$ java CountFixed.java
loop: 2 failed
stream: 2 failed
AtomicInteger: 2 failed
passed: 3
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 capture_session.py
The specification requires every local variable a lambda uses but does not declare to be final or effectively
final (§15.27.2). A variable is effectively final when it is never assigned again after it gets its value and is
never incremented or decremented (§4.12.4), so that adding final would change nothing. failed++ breaks that.
The rule protects you: a lambda captures the value of the variable and may run later, after the method has
returned, or on another thread. If the variable could still change, the lambda’s copy would silently disagree with
it, and changes from several threads would race. The specification names that risk as its reason for the rule.
CountFixed.java shows three ways out:
- A plain loop. No lambda, so nothing is captured and the local may change freely.
- Ask the stream for the count.
filter(…).count()leaves nothing to change at all. This is usually the clearest choice. - A mutable object.
counteritself never changes; theAtomicIntegerit points to does.AtomicIntegeralso stays correct if the stream later runs in parallel. A one-element array,int[] failed = {0}, compiles for the same reason but is not safe across threads.
The last line reads passMark inside a lambda, which is fine because it is assigned once. Write passMark = 35;
anywhere else in the method, even after the lambda, and the lambda stops compiling: the rule is about the variable,
not about the order of the statements. Fields are different: a lambda reaches an instance field through this,
which never changes, and a static field through its class, so it may update fields, with the same thread-safety
questions as any other code.
What this means inside a lambda
An anonymous class is a class, so inside it this is the anonymous object. A lambda is not a new scope for names:
inside its body, this, super and every name other than its own parameters mean exactly what they mean in the code
around it (§15.27.2).
class Report {
private final String title = "Monthly report";
void show() {
Runnable anonymous = new Runnable() {
@Override
public void run() {
IO.println("anonymous class: this is a " + this.getClass().getName());
IO.println(" the title is " + Report.this.title);
}
};
Runnable lambda = () -> {
IO.println("lambda: this is a " + this.getClass().getName());
IO.println(" the title is " + this.title);
};
anonymous.run();
lambda.run();
}
}
class ThisDemo {
static void main() {
new Report().show();
}
} import java.util.List;
class Shadow {
static void main() {
String name = "Asha";
List.of("Ravi", "Meera").forEach(name -> IO.println(name));
}
} from session import run
run("""
java ThisDemo.java
java Shadow.java
""", fails=("java Shadow.java",)) Output
# javac 25.0.4.1
$ java ThisDemo.java
anonymous class: this is a Report$1
the title is Monthly report
lambda: this is a Report
the title is Monthly report
$ java Shadow.java
Shadow.java:6: error: variable name is already defined in method main()
List.of("Ravi", "Meera").forEach(name -> IO.println(name));
^
1 error
error: compilation failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 this_session.py
In the anonymous class, this.title would not even compile, because there this is the Runnable, so it reaches the
report as Report.this.title (or as plain title). In the lambda, this.title is the report’s title. The same rule explains the error in Shadow.java: because the lambda shares the scope of main, its
parameter may not reuse the name of the local variable name. Inside a lambda you also cannot refer to the lambda
itself through this. When an object needs its own state or several methods, write a class.
What javac makes of a lambda
The anonymous class left a .class file behind. To see where a lambda’s code goes, compile a small class and
disassemble it with javap, the class-file disassembler that comes with the JDK:
import java.util.function.IntPredicate;
class Rules {
static IntPredicate passMark() {
return m -> m >= 40;
}
static IntPredicate atLeast(int min) {
return m -> m >= min;
}
} class SameObject {
static void main() {
IO.println(Rules.passMark().test(82));
IO.println("hidden class: " + Rules.passMark().getClass().isHidden());
// Each call evaluates the lambda expression again: one object, or a new one each time?
IO.println("no capture, same object: " + (Rules.passMark() == Rules.passMark()));
IO.println("capture, same object: " + (Rules.atLeast(40) == Rules.atLeast(40)));
}
} from session import run
run("""
javac -d classes Rules.java SameObject.java
ls classes
javap -c -p classes/Rules.class
javap -v classes/Rules.class | grep -A 1 '^BootstrapMethods:'
java -cp classes SameObject
""") Output
# javac 25.0.4.1
$ javac -d classes Rules.java SameObject.java
$ ls classes
Rules.class
SameObject.class
$ javap -c -p classes/Rules.class
Compiled from "Rules.java"
class Rules {
Rules();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static java.util.function.IntPredicate passMark();
Code:
0: invokedynamic #7, 0 // InvokeDynamic #0:test:()Ljava/util/function/IntPredicate;
5: areturn
static java.util.function.IntPredicate atLeast(int);
Code:
0: iload_0
1: invokedynamic #11, 0 // InvokeDynamic #1:test:(I)Ljava/util/function/IntPredicate;
6: areturn
private static boolean lambda$atLeast$0(int, int);
Code:
0: iload_1
1: iload_0
2: if_icmplt 9
5: iconst_1
6: goto 10
9: iconst_0
10: ireturn
private static boolean lambda$passMark$0(int);
Code:
0: iload_0
1: bipush 40
3: if_icmplt 10
6: iconst_1
7: goto 11
10: iconst_0
11: ireturn
}
$ javap -v classes/Rules.class | grep -A 1 '^BootstrapMethods:'
BootstrapMethods:
0: #35 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
$ java -cp classes SameObject
true
hidden class: true
no capture, same object: true
capture, same object: false
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bytecode_session.py
- The body became a private static method of
Rules:lambda$passMark$0(int). The capturing lambda’s method,lambda$atLeast$0(int, int), has one more parameter, the capturedmin, placed before the lambda’s own parameter. That is what capturing means at the bytecode level: a value passed along when the object is made. - The method that evaluates the lambda does it with one
invokedynamicinstruction, not anew. Its bootstrap method, listed underBootstrapMethods, isLambdaMetafactory.metafactory. The first time that instruction runs, the JVM calls the bootstrap method, which creates a class implementingIntPredicatewhosetestmethod calls the private method. The class is a hidden class, made in memory, which is why no file appeared on disk. - The last lines show something you must not rely on. On JDK 25, evaluating the lambda without captures twice
gave the same object, and the capturing one gave a new object each time. The specification allows either
(§15.27.4), so never compare lambdas with
==, lock on them or use their identity hash codes.
Lambda, method reference or named method?
The three are interchangeable for the compiler, so the choice is about the reader:
- A lambda when the behaviour is short, used once and best read where it is used, often with a captured value:
m -> m >= passMark. - A method reference when an existing method already does exactly the job:
Student::marks,Integer::parseInt. - A named method when the body needs several lines, a name to explain it, a test of its own or more than one
caller:
static String grade(int marks), passed asGrades::grade.
A useful test: if you have to read the body of a lambda twice to know what it does, give it a name. The grade
lambda in LambdaForms.java already fails that test, and as a method it could be tested on its own. Method
references, the short form for “call this existing method”, are the third lesson of this module.
Your turn: applyAll and a filter pipeline
Write two methods, then check them against the cases in main:
applyAll(List<Integer> xs, List<IntUnaryOperator> ops)returns a new list in which each number has had every operator applied, in the order of the list. With no operators it returns the numbers unchanged.keepAll(List<String> words, List<Predicate<String>> rules)keeps the words that pass every rule. The rules run in order, and a word’s checks stop at the first rule it fails.
The second case matters more than it looks: one rule calls w.charAt(0), which throws for the empty string, so the
rule that rejects empty words has to run first and stop the checks. Write your version before you read the one below.
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntUnaryOperator;
import java.util.function.Predicate;
class ApplyAll {
/** Applies every operator, in list order, to each number. */
static List<Integer> applyAll(List<Integer> xs, List<IntUnaryOperator> ops) {
List<Integer> out = new ArrayList<>();
for (int x : xs) {
int value = x;
for (IntUnaryOperator op : ops) value = op.applyAsInt(value);
out.add(value);
}
return out;
}
/** Keeps the words that pass every rule; rules run in order and stop at the first failure. */
static List<String> keepAll(List<String> words, List<Predicate<String>> rules) {
List<String> out = new ArrayList<>();
for (String w : words) {
boolean keep = true;
for (Predicate<String> rule : rules) {
if (!rule.test(w)) {
keep = false;
break;
}
}
if (keep) out.add(w);
}
return out;
}
static void check(String what, Object got, Object want) {
IO.println((got.equals(want) ? "ok " : "WRONG ") + what + ": " + got);
}
static void main() {
List<IntUnaryOperator> ops = List.of(x -> x + 1, x -> x * 10);
check("add 1, then times 10", applyAll(List.of(1, 2, 3), ops), List.of(20, 30, 40));
check("the other order", applyAll(List.of(1, 2, 3), List.of(x -> x * 10, x -> x + 1)), List.of(11, 21, 31));
check("no operators", applyAll(List.of(5, 6), List.of()), List.of(5, 6));
check("no numbers", applyAll(List.of(), ops), List.of());
List<Predicate<String>> rules = List.of(
w -> !w.isEmpty(), // must run first: charAt(0) needs a character
w -> Character.isUpperCase(w.charAt(0)),
w -> w.length() <= 5);
check("three rules", keepAll(List.of("Asha", "ravi", "", "Meenakshi", "Kabir"), rules), List.of("Asha", "Kabir"));
check("no rules", keepAll(List.of("a", "b"), List.of()), List.of("a", "b"));
}
} from session import run
run("""
java ApplyAll.java
""") Output
# javac 25.0.4.1 $ java ApplyAll.java ok add 1, then times 10: [20, 30, 40] ok the other order: [11, 21, 31] ok no operators: [5, 6] ok no numbers: [] ok three rules: [Asha, Kabir] ok no rules: [a, b]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
The early break in keepAll is what keeps the empty string away from charAt(0). Predicate.and, which the next
lesson uses to join rules, stops at the first failing rule in the same way.
Interview questions
Fresher · What does effectively final mean, and why must local variables used in a lambda be effectively final?
A local variable is effectively final when it is never assigned again after it gets its value and is never incremented or decremented, so adding final would change nothing. A lambda that uses such a variable captures its value, and the lambda may run later, after the method has returned, or on another thread. If the variable could still change, the copy inside the lambda and the variable could disagree, and updates from several threads would race, so the Java Language Specification forbids it. javac reports that local variables referenced from a lambda expression must be final or effectively final. To count or collect, let the stream do it with count() or collect(), use a plain loop, or keep a mutable object such as an AtomicInteger in an effectively final variable.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A colleague counts invalid orders with int[] invalid = {0}; orders.forEach(o -> { if (!valid(o)) invalid[0]++; }), because a plain int would not compile. Review the change, and say what happens when forEach becomes orders.parallelStream().forEach.
-
Mid to senior
You are converting a large codebase's anonymous Runnable, Comparator and listener classes to lambdas with an IDE quick-fix. Which conversions can change behaviour, and how would you find them before they reach production?
Key takeaways
- A lambda is an object of a functional interface. The interface is its target type, and it comes from the context:
an assignment, a
return, a method argument or a cast. The same text can have several types. - The target type also decides the parameter and result types:
mis anintforIntPredicateand anIntegerforPredicate<Integer>. var f = x -> …andObject o = () -> …do not compile: neither gives the lambda a functional interface.- Local variables used in a lambda must be effectively final. Count with the stream, use a loop, or hold a mutable
object such as an
AtomicInteger. thisin a lambda is the object of the surrounding code, and a lambda parameter may not reuse a local’s name.javaccompiles a lambda body into a private method and aninvokedynamiccall site; the class is made in memory at run time. Never rely on the identity of a lambda object.- Keep lambdas short. Prefer a method reference when a method already does the job, and a named method when the body needs a name or a test.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- The Java Language Specification, Java SE 25 Edition: 15.27 Lambda Expressions (Oracle)
- The Java Language Specification, Java SE 25 Edition: 4.12.4 final Variables (Oracle)
- The Java Language Specification, Java SE 25 Edition: 9.8 Functional Interfaces (Oracle)
- Package java.util.function (Java SE 25) (Oracle)
- LambdaMetafactory (Java SE 25) (Oracle)
- The javap Command (JDK 25) (Oracle)
- JEP 323: Local-Variable Syntax for Lambda Parameters (OpenJDK)
- JEP 456: Unnamed Variables & Patterns (OpenJDK)
- AtomicInteger (Java SE 25) (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress