Lambda Expressions
Lambda expressions, introduced in Java 8, are anonymous functions — blocks of code that can be stored in variables, passed as arguments, and returned from methods, treating behavior as data. A lambda has three parts: a parameter list, an arrow token (->), and a body. The body is either a single expression (whose value is the implicit return value) or a block of statements wrapped in braces. Lambdas implement functional interfaces — interfaces with exactly one abstract method — allowing any lambda whose signature matches the abstract method's signature to be used wherever that interface is expected. The lambda syntax is syntactic sugar: every lambda is compiled to an invocation of the functional interface's abstract method, with the compiler generating a class (via invokedynamic) that implements the interface and delegates to the lambda body. This entry covers the complete lambda syntax including all shorthand forms, variable capture and the effectively-final constraint, method references as a specialized lambda syntax, the relationship between lambdas and the type system, how lambdas interact with exception handling, the invokedynamic compilation strategy and its performance characteristics, and the complete set of rules governing lambda type inference.
Lambda Syntax — All Forms and Shorthand Rules
// ── Lambda syntax: all forms ──────────────────────────────────────────
// No parameters:
Runnable r1 = () -> System.out.println("no params");
Runnable r2 = () -> { System.out.println("block body"); System.out.println("line 2"); };
// Single parameter — parens optional when type is inferred:
Consumer<String> c1 = s -> System.out.println(s); // no parens
Consumer<String> c2 = (s) -> System.out.println(s); // with parens — equivalent
Consumer<String> c3 = (String s) -> System.out.println(s); // explicit type
// Multiple parameters:
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; // expression body
BiFunction<Integer, Integer, Integer> div = (a, b) -> { return a / b; }; // block body
// Expression body vs block body:
Function<Integer, Integer> doubleExpr = x -> x * 2; // expression
Function<Integer, Integer> doubleBlock = x -> { return x * 2; }; // block — equivalent
// Void lambda — expression body must be void-returning:
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
Consumer<List<String>> clearer = l -> l.clear(); // clear() returns void — OK
// Consumer<List<String>> wrong = l -> l.size(); // size() returns int — NOT void
// ── Method references — all four forms ───────────────────────────────
// 1. Static method reference: ClassName::staticMethod
Function<String, Integer> parseInt1 = Integer::parseInt;
Function<String, Integer> parseInt2 = s -> Integer.parseInt(s); // equivalent lambda
// 2. Instance method on specific instance: instance::method
String prefix = "Hello, ";
Function<String, String> greet1 = prefix::concat;
Function<String, String> greet2 = s -> prefix.concat(s); // equivalent
// 3. Instance method on arbitrary instance: ClassName::instanceMethod
Function<String, String> upper1 = String::toUpperCase;
Function<String, String> upper2 = s -> s.toUpperCase(); // equivalent
BiFunction<String, String, Boolean> startsWith1 = String::startsWith;
BiFunction<String, String, Boolean> startsWith2 = (s, t) -> s.startsWith(t); // equivalent
// 4. Constructor reference: ClassName::new
Supplier<ArrayList<String>> listFactory1 = ArrayList::new;
Supplier<ArrayList<String>> listFactory2 = () -> new ArrayList<>(); // equivalent
Function<Integer, ArrayList<String>> sizedList = ArrayList::new; // sized constructor
Function<Integer, ArrayList<String>> sizedList2 = n -> new ArrayList<>(n); // equivalent
// ── Method references in stream pipelines ─────────────────────────────
List<String> words = List.of("hello", "world", "java", "lambda");
// Using method references instead of lambdas:
words.stream()
.filter(String::isEmpty) // ClassName::instanceMethod
.map(String::toUpperCase) // ClassName::instanceMethod
.sorted(String::compareTo) // ClassName::instanceMethod (Comparator)
.forEach(System.out::println); // instance::method on System.out
// Constructor reference for collecting:
List<String> collected = words.stream()
.collect(Collectors.toCollection(ArrayList::new));
// ── Explicit type annotation when inference fails ─────────────────────
// Sometimes the compiler can't infer parameter types:
// Comparator<String> c = (a, b) -> a.compareTo(b); — works
// When overloads create ambiguity, use explicit types:
// Collections.sort(list, (String a, String b) -> a.compareTo(b)); // explicit typesVariable Capture, Effectively Final, and Closure Semantics
// ── Effectively final — what is and isn't allowed ────────────────────
int x = 10;
// x = 20; // if this line were here, x would NOT be effectively final
Supplier<Integer> s = () -> x; // OK: x assigned once — effectively final
System.out.println(s.get()); // 10
// Mutation attempt — compile error:
// int y = 10;
// Supplier<Integer> bad = () -> {
// y++; // COMPILE ERROR: y is not effectively final
// return y;
// };
// Workaround 1: AtomicInteger (reference is effectively final, value is mutable)
AtomicInteger counter = new AtomicInteger(0);
Runnable increment = () -> counter.incrementAndGet(); // OK: counter ref is final
increment.run(); increment.run();
System.out.println(counter.get()); // 2
// Workaround 2: single-element array
int[] mutableInt = {0};
Runnable addToArray = () -> mutableInt[0]++; // OK: mutableInt ref is final
addToArray.run(); addToArray.run();
System.out.println(mutableInt[0]); // 2 (only safe if used on single thread)
// ── Instance fields: no effectively-final restriction ─────────────────
class Counter {
private int count = 0;
Runnable createIncrementer() {
return () -> count++; // OK: count is an instance field, not a local variable
}
Supplier<Integer> createGetter() {
return () -> count; // captures 'this' implicitly
}
}
Counter c = new Counter();
c.createIncrementer().run();
c.createIncrementer().run();
System.out.println(c.createGetter().get()); // 2
// ── this reference: lambda vs anonymous class ─────────────────────────
class MyClass {
String name = "MyClass";
Runnable lambdaThis = () -> System.out.println(this.name); // this = MyClass instance
Runnable anonThis = new Runnable() {
String name = "AnonClass";
@Override
public void run() {
System.out.println(this.name); // this = the Runnable anonymous instance
}
};
void demonstrate() {
lambdaThis.run(); // "MyClass"
anonThis.run(); // "AnonClass"
}
}
// ── Capturing object references — reference is final, contents mutable ─
List<String> capturedList = new ArrayList<>();
capturedList.add("initial");
Consumer<String> adder = item -> capturedList.add(item); // OK: capturedList ref is final
adder.accept("added by lambda");
capturedList.add("added directly");
System.out.println(capturedList); // [initial, added by lambda, added directly]
// Lambda sees all mutations to the list — captures reference, not snapshot
// ── Recursive lambda — requires field, not local variable ──────────────
// WRONG — does not compile:
// Function<Integer, Integer> factorial = n ->
// n == 0 ? 1 : n * factorial.apply(n - 1); // factorial not yet initialized
// CORRECT — use a field:
class Recursion {
Function<Integer, Integer> factorial;
Recursion() {
factorial = n -> n == 0 ? 1 : n * factorial.apply(n - 1);
}
}
System.out.println(new Recursion().factorial.apply(5)); // 120
// Or with explicit array trick:
Function<Integer, Integer>[] holder = new Function[1];
holder[0] = n -> n == 0 ? 1 : n * holder[0].apply(n - 1); // holder ref is final
System.out.println(holder[0].apply(5)); // 120Lambda vs Anonymous Class, Exception Handling, and Performance
// ── Lambda vs anonymous class — side-by-side comparison ──────────────
// Anonymous class:
Runnable anon = new Runnable() {
@Override
public void run() {
System.out.println("anonymous class: " + this.getClass().getName());
}
};
// Lambda — equivalent behavior, different semantics:
Runnable lambda = () -> System.out.println("lambda: " + this.getClass().getName());
// 'this' in the lambda refers to the ENCLOSING class, not the lambda
anon.run(); // anonymous class: com.example.MyClass$1
lambda.run(); // lambda: com.example.MyClass
// Identity: lambdas without capture may share instance
Runnable la = () -> {};
Runnable lb = () -> {}; // may or may not be the same object — not guaranteed
System.out.println(la == lb); // JVM-defined — often true for stateless lambdas
// Anonymous class always creates a new instance:
Runnable aa = new Runnable() { @Override public void run() {} };
Runnable ab = new Runnable() { @Override public void run() {} };
System.out.println(aa == ab); // always false
// ── Exception handling — checked exceptions in lambdas ────────────────
// Checked exception in stream — compile error:
List<String> paths = List.of("/etc/passwd", "/etc/hosts");
// COMPILE ERROR — Files.readString throws IOException, but Function doesn't declare it:
// List<String> contents = paths.stream()
// .map(p -> Files.readString(Path.of(p))) // ERROR: unreported exception IOException
// .collect(Collectors.toList());
// Solution 1: catch inside lambda, wrap as unchecked:
List<String> contents = paths.stream()
.map(p -> {
try {
return Files.readString(Path.of(p));
} catch (IOException e) {
throw new UncheckedIOException(e); // UncheckedIOException extends RuntimeException
}
})
.collect(Collectors.toList());
// Solution 2: wrapper utility method
@FunctionalInterface
interface CheckedFunction<T, R> {
R apply(T t) throws Exception;
}
static <T, R> Function<T, R> wrap(CheckedFunction<T, R> f) {
return t -> {
try { return f.apply(t); }
catch (RuntimeException e) { throw e; }
catch (Exception e) { throw new RuntimeException(e); }
};
}
List<String> contents2 = paths.stream()
.map(wrap(p -> Files.readString(Path.of(p)))) // cleaner
.collect(Collectors.toList());
// Solution 3: custom functional interface that declares the exception:
@FunctionalInterface
interface IOFunction<T, R> {
R apply(T t) throws IOException;
}
// Can use this with explicit try-catch at the call site:
IOFunction<String, String> reader = p -> Files.readString(Path.of(p));
try {
String content = reader.apply("/etc/passwd");
} catch (IOException e) { /* handle */ }
// ── Performance: stateless vs capturing lambdas ────────────────────────
// Stateless lambda — typically a singleton, allocated once:
Predicate<String> isEmpty = String::isEmpty; // stateless method reference
// JVM: same object reference every time this lambda is instantiated at this site
// Capturing lambda — new instance each time:
String prefix = "Hello";
Predicate<String> startsWith = s -> s.startsWith(prefix);
// JVM: new instance allocated each time this line executes
// Contains a reference to the captured String 'prefix'
// Benchmark implication: in tight loops, avoid capturing lambdas inside the loop
// BAD: creates new lambda instance each iteration
for (int i = 0; i < 1_000_000; i++) {
final int threshold = i;
list.stream().filter(x -> x > threshold).count(); // new lambda each iteration
}
// BETTER: hoist lambda creation outside loop
IntPredicate threshold5 = x -> x > 5; // created once, reused
for (int i = 0; i < 1_000_000; i++) {
list.stream().mapToInt(Integer::intValue).filter(threshold5).count();
}