☕ Java

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

A lambda expression has the form (parameters) -> body. The parameter list, arrow, and body are all mandatory, but each has multiple valid forms that the compiler accepts depending on context. The parameter list can take four forms. When there are no parameters, empty parentheses are required: () -> body. When there is exactly one parameter and its type can be inferred, the parentheses are optional: x -> body or (x) -> body, both valid. When there are multiple parameters, parentheses are required: (x, y) -> body. When parameter types are provided explicitly, parentheses are required even for a single parameter: (int x) -> body or (String s, int n) -> body. Mixing explicit and inferred types is illegal: (String s, n) -> body does not compile — either all types are explicit or all are inferred. The body can take two forms. An expression body is a single expression without braces: x -> x * 2. The expression's value is the implicit return value; no return keyword is used. A block body is one or more statements in braces: x -> { return x * 2; }. Block bodies require an explicit return statement for non-void lambdas. A lambda that returns void may have an expression body that is a void-returning method call: list -> list.clear(). The compiler infers the lambda's type from the context in which it appears — this is called the target type. A lambda appearing as an argument to a method accepting Predicate<Integer> has target type Predicate<Integer>; the compiler checks that the lambda's parameter and return types match Predicate's abstract method boolean test(T t). A lambda can only be used in a context where a target type is available: assignment to a typed variable, argument to a typed method parameter, return from a typed method, or cast. Lambdas have no type of their own — they cannot be assigned to Object or stored without a target type. Method references are lambdas whose body is a single method call, written as ClassName::methodName or instance::methodName. The four forms are: static method reference (ClassName::staticMethod — equivalent to (args) -> ClassName.staticMethod(args)), instance method reference on a specific instance (instance::method — equivalent to (args) -> instance.method(args)), instance method reference on an arbitrary instance of a class (ClassName::instanceMethod — equivalent to (obj, args) -> obj.method(args), where obj is the first argument), and constructor reference (ClassName::new — equivalent to (args) -> new ClassName(args)).
Java
// ── 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 types

Variable Capture, Effectively Final, and Closure Semantics

Lambda expressions can capture variables from their enclosing scope — the enclosing method, constructor, or initializer block. The captured variables must be either declared final or effectively final. A variable is effectively final if it is assigned exactly once from its declaration to the end of its scope — the compiler enforces this even without the final keyword. Attempting to capture a variable that is assigned more than once causes a compile error: "local variable used in lambda expression must be final or effectively final." This restriction exists because lambda instances can outlive the method call in which they were created. When a lambda is passed to an executor and runs on another thread after the creating method has returned, the local variable no longer exists on the stack. Java solves this by copying the variable's value into the lambda's closure at the time of lambda creation. A mutable variable would require synchronization — the copy in the closure and the variable in the original method would diverge. By requiring effectively final, Java ensures the copy is always consistent with the original. Instance fields and static fields are not subject to the effectively-final restriction because they exist on the heap, not the stack, and are accessible to any thread at any time. A lambda can read and write instance fields freely. This distinction leads to a common workaround for the effectively-final restriction: wrapping a mutable value in an array or an AtomicInteger — both of which are effectively-final objects whose contents can change. This is correct for object mutation but introduces thread-safety concerns if the lambda runs on a different thread. The this reference inside a lambda refers to the enclosing class instance, not to the lambda itself (lambdas have no identity — they are not objects in the traditional sense). This differs from anonymous classes, where this refers to the anonymous class instance. This means lambdas cannot call super, cannot be synchronized on, and cannot be used as monitors. It also means lambdas cannot directly reference themselves (for recursion) — a recursive lambda must assign itself to a variable and capture that variable, but since the lambda itself is the initializer for that variable, the variable is not yet initialized when the lambda captures it. The workaround is to use a field rather than a local variable. Lambdas capture the value of local variables but capture the reference of object variables. When a lambda captures a List, it captures the reference to the list object — subsequent modifications to the list by other code are visible to the lambda when it executes. Only the reference must be effectively final; the object it points to can be mutated freely.
Java
// ── 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));  // 120

Lambda vs Anonymous Class, Exception Handling, and Performance

Lambdas and anonymous classes both implement functional interfaces, but they differ in semantics, performance, and capabilities. Anonymous classes are full Java classes with their own identity, their own this reference, their own scope for variable declarations, and the ability to implement multiple methods or extend a class. They are objects in the traditional sense — each new Runnable() {} creates a distinct object with an identity. Lambdas have no identity of their own — the JVM may return the same lambda instance for every invocation if the lambda captures no state, optimizing memory usage for stateless lambdas. The compilation strategy for lambdas uses invokedynamic rather than generating a new class file at compile time. The first time the lambda site is executed, the JVM invokes a bootstrap method that generates the implementing class dynamically, using java.lang.invoke.LambdaMetafactory. This defers class generation to runtime and gives the JVM freedom to optimize (cache stateless lambda instances, inline the lambda body). For stateless lambdas, the JVM typically generates a singleton — one instance reused for all invocations. For capturing lambdas, a new instance is created each time the lambda expression is evaluated, which has the same allocation cost as an anonymous class instance. Exception handling in lambdas follows the functional interface's abstract method declaration. If the abstract method declares no checked exceptions, the lambda body cannot throw checked exceptions — they must be caught inside the lambda or converted to unchecked exceptions. Function<T,R>, Predicate<T>, and Consumer<T> declare no checked exceptions; lambdas implementing them cannot propagate IOException, SQLException, or other checked exceptions. This is the most common source of friction between lambdas and legacy APIs that throw checked exceptions. The workarounds are: catch the checked exception inside the lambda and wrap it in an unchecked exception (RuntimeException or a custom unchecked wrapper); define a custom functional interface that declares the checked exception (FunctionThatThrows<T, R, E extends Exception> with R apply(T t) throws E); or use utility methods like Unchecked.function() from libraries such as vavr or the Apache Commons Lang ExceptionUtils. For Java stream pipelines that process I/O, wrapping in UncheckedIOException (a RuntimeException subclass introduced in Java 8 specifically for this purpose) is idiomatic.
Java
// ── 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();
}

Related Topics in Java 8 Features

Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
Predicate
Predicate<T> is a functional interface in java.util.function representing a boolean-valued function of one argument, with the single abstract method boolean test(T t). It is one of the four foundational functional interfaces in the Java standard library and is used throughout the Collections framework, Streams API, and Optional for filtering, condition testing, and validation. Predicate is designed for composition: its default methods and(Predicate), or(Predicate), and negate() allow building complex boolean expressions from simple predicates without boilerplate. The static methods isEqual(Object) and not(Predicate) provide factory methods for common cases. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate avoid boxing overhead for numeric values. BiPredicate<T,U> extends the concept to two-argument boolean functions. This entry covers the complete Predicate API, all composition methods and their short-circuit semantics, the static factory methods, primitive specializations, BiPredicate, using Predicate in stream pipelines and Collections methods, building validation frameworks with Predicate composition, and the performance and readability trade-offs of different composition styles.
Function
Function<T,R> is a functional interface in java.util.function representing a function that accepts one argument of type T and produces a result of type R, with the single abstract method R apply(T t). It is the most general transformation interface in the standard library, used throughout the Streams API for mapping (Stream.map()), in Optional for value transformation (Optional.map(), Optional.flatMap()), and as a building block for more specialized functional interfaces. Function provides two default composition methods — andThen() and compose() — that create new functions by chaining two functions together, enabling functional pipeline construction without intermediate variables. The specializations cover all combinations of generic and primitive inputs and outputs: ToIntFunction, IntFunction, IntToLongFunction, and so on. UnaryOperator<T> extends Function<T,T> for operations that transform a value within the same type. BiFunction<T,U,R> generalizes to two input arguments. This entry covers the complete Function API, the semantics of andThen versus compose, all specializations and when each is appropriate, the functional relationship between Function and other java.util.function types, partial application patterns, and Function as the basis for building data pipelines.
Consumer
Consumer<T> is a functional interface in java.util.function that represents an operation that accepts a single input argument and returns no result. It is the standard abstraction for side-effect-oriented operations — printing, logging, persisting, modifying in-place, or any action that consumes a value without producing a new one. Consumer<T> declares one abstract method, accept(T t), and one default method, andThen(Consumer<? super T> after), which composes two consumers sequentially: the second consumer receives the same input as the first, enabling chaining of multiple side effects on the same value. The java.util.function package also provides primitive specializations — IntConsumer, LongConsumer, DoubleConsumer — that avoid boxing overhead, and BiConsumer<T, U> for operations on two arguments. Consumer is used throughout the Stream API (forEach), the Collections API (Iterable.forEach, Map.forEach), and is the standard type for callback and event-handler parameters. This entry covers the full Consumer API including andThen composition, all primitive specializations and BiConsumer, the distinction between Consumer and other functional interfaces, common usage patterns in streams and collections, and how to write effective Consumer implementations.