Lambda Expressions

Lambda expressions let you write a block of behavior as a compact, inline value — turning verbose anonymous classes into a few readable characters.

Introduced in Java 8, lambda expressions provide a concise syntax for implementing a functional interface (an interface with exactly one abstract method) inline, without the ceremony of a full anonymous class. They are central to Java's functional-programming-flavored APIs, especially the Streams API.

What Is a Lambda Expression

A lambda expression is an anonymous block of code that can be passed around as a value, matching the shape of some functional interface's single abstract method. It has the general form (parameters) -> expression or (parameters) -> { statements }.

import java.util.function.BinaryOperator;

BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(3, 4)); // 7

Lambda Syntax and Parameters

Parameter lists can omit types (inferred from the target functional interface's method), omit parentheses entirely for a single untyped parameter, or spell out explicit types for clarity.

import java.util.function.Function;

Function<Integer, Integer> square1 = x -> x * x;                  // single param, no parens needed
Function<Integer, Integer> square2 = (x) -> x * x;                // parens optional here
Function<Integer, Integer> square3 = (Integer x) -> x * x;        // explicit type

import java.util.function.BiFunction;
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; // multiple params need parens

Expression Body vs Block Body

An expression body (-> expr) implicitly returns the expression's value, with no return keyword or braces. A block body (-> { ... }) can contain multiple statements and needs an explicit return if the functional interface's method has a non-void result.

Function<Integer, String> label1 = n -> n > 0 ? "positive" : "non-positive"; // expression body

Function<Integer, String> label2 = n -> {          // block body
    if (n > 0) {
        return "positive";
    }
    return "non-positive";
};

Target Typing

A lambda has no type of its own — its meaning comes entirely from the functional interface type it's being assigned to or passed as (the 'target type'). The compiler infers parameter types and validates the body against that interface's single abstract method signature.

Runnable r = () -> System.out.println("running"); // target type: Runnable.run()
Comparator<String> cmp = (a, b) -> a.length() - b.length(); // target type: Comparator.compare(T,T)
// The SAME lambda syntax (a, b) -> ... means something different depending on target type

Functional Interfaces Connection

A lambda can only be used where a functional interface (an interface with exactly one abstract method, optionally annotated @FunctionalInterface) is expected. java.util.function provides many general-purpose ones (Function, Predicate, Consumer, Supplier, etc.), covered in depth in the Functional Interfaces topic.

import java.util.function.Predicate;

Predicate<String> isLong = s -> s.length() > 10;
System.out.println(isLong.test("hello"));       // false
System.out.println(isLong.test("hello world!")); // true

Capturing Local Variables: Effectively Final

A lambda can capture (reference) local variables from its enclosing scope, but only if those variables are final or 'effectively final' — assigned exactly once and never reassigned afterward. This is enforced at compile time.

This restriction exists because a lambda may outlive the method call that created it (e.g. stored and invoked later); capturing by value at creation time only makes sense for variables that never change, avoiding a whole class of stale/inconsistent-state bugs.

int threshold = 10; // effectively final - never reassigned
Predicate<Integer> isAboveThreshold = n -> n > threshold; // OK, captures threshold

int counter = 0;
Runnable broken = () -> {
    // counter++; // would NOT compile: counter is reassigned elsewhere, so it's not effectively final
};
counter++; // this reassignment is what disqualifies 'counter' from being captured above

Capturing 'this'

Unlike an anonymous inner class, a lambda does not introduce its own this — this inside a lambda refers to the enclosing instance, exactly as if the lambda's body were written directly in the surrounding method. This differs from anonymous classes, where this refers to the anonymous class instance itself.

public class Counter {
    private int count = 0;

    public Runnable incrementer() {
        return () -> {
            this.count++; // 'this' is the Counter instance, not the lambda
        };
    }
}

Method References as Lambda Shorthand

When a lambda's body does nothing more than call an existing method with the lambda's parameters, a method reference (::) is often a more readable substitute. This is covered in full in the Method References topic.

List<String> names = new ArrayList<>(List.of("Carl", "ann", "Bob"));
names.sort((a, b) -> a.compareToIgnoreCase(b));    // lambda
names.sort(String::compareToIgnoreCase);            // equivalent method reference

Rules and Behavior

A lambda body can throw checked exceptions only if the functional interface's abstract method declares them. Lambdas cannot declare their own type parameters (generics) and cannot have a name — they are inherently anonymous, single-use values (though they can be assigned to a variable for reuse).

Practical Usage

Lambdas shine as arguments to Streams operations, event handlers, and any API expecting 'a piece of behavior' — sorting, filtering, callbacks, and deferred computation.

List<String> words = List.of("banana", "kiwi", "apple", "fig");
List<String> shortWords = words.stream()
    .filter(w -> w.length() <= 4)
    .toList();
System.out.println(shortWords); // [kiwi, fig]

Edge Cases

A lambda cannot capture a variable declared later in the enclosing scope. Overload resolution can occasionally be ambiguous when a lambda could match multiple functional interfaces with identical method signatures — an explicit cast to the intended interface resolves it.

// Ambiguous overloads example (illustrative):
// void process(Runnable r) {}
// void process(Callable<String> c) {}
// process(() -> { return "x"; }); // ambiguous - needs explicit cast: (Callable<String>) () -> "x"

Complete Example

import java.util.*;
import java.util.function.*;

public class LambdaDemo {
    public static void main(String[] args) {
        List<String> items = new ArrayList<>(List.of("pear", "Apple", "banana", "Fig"));

        // Sort case-insensitively using a lambda
        items.sort((a, b) -> a.compareToIgnoreCase(b));

        // Filter with a lambda-backed Predicate
        Predicate<String> startsWithVowel = s -> "AEIOUaeiou".indexOf(s.charAt(0)) >= 0;

        for (String item : items) {
            if (startsWithVowel.test(item)) {
                System.out.println(item + " starts with a vowel");
            }
        }
    }
}

Mental Model

Think of a lambda as an anonymous, portable 'sticky note' of behavior: it captures the values (not future changes) of the variables it needs from its surroundings, and can then be handed off, stored, or invoked later — anywhere a matching functional interface is expected.

Quick Reference

() -> expr                    // no params, expression body
x -> expr                      // one param, no parens needed
(x) -> expr                    // one param, parens optional
(x, y) -> expr                 // multiple params, parens required
(Type x, Type y) -> expr       // explicit types
x -> { stmt; stmt; return v; } // block body, needs explicit return

Key Takeaways

A lambda expression provides a compact inline implementation of a functional interface's single abstract method.

Its meaning comes entirely from target typing — the same syntax means different things depending on the expected functional interface.

Captured local variables must be final or effectively final; this is enforced at compile time.

'this' inside a lambda refers to the enclosing instance, unlike in an anonymous inner class.

Method references (::) are often a cleaner substitute when a lambda just forwards its arguments to an existing method.

int total = 0; list.forEach(n -> total += n); // does not compile: total is not effectively final int[] total = {0}; list.forEach(n -> total[0] += n); // or better, use IntStream.sum() / reduce()

Lambdas can only capture effectively final local variables; a mutated accumulator like this needs a mutable holder (or, better, a proper reduction like stream().mapToInt(...).sum()).

Comparator<String> cmp = (String a, String b) -> { a.length() - b.length() }; // missing return / semicolon, won't compile

Comparator<String> cmp = (a, b) -> a.length() - b.length(); // expression body, implicit return

A block body { ... } requires an explicit return statement for a non-void method; use an expression body to avoid the boilerplate when the body is a single expression.

Runnable r = () -> { this.count++; // assuming a lambda gets its own 'this', confusing anonymous-class habits

// Understand that 'this' in a lambda IS the enclosing instance already - no special handling needed Runnable r = () -> this.count++;

Unlike anonymous inner classes, lambdas don't create a new 'this' scope — this refers to the enclosing instance directly, which is usually what you want and requires no workaround.

What determines the meaning of a lambda expression's parameters and return type?

Target typing: the functional interface the lambda is being assigned to or passed as determines the parameter types and expected return type, since the lambda itself has no inherent type.

Why must local variables captured by a lambda be effectively final?

A lambda may be invoked after its enclosing method has returned, so it captures the variable's value at creation time; allowing reassignment afterward would create ambiguity about which value the lambda should see, so the compiler forbids capturing a variable that's reassigned anywhere in its scope.

How does 'this' behave differently inside a lambda versus inside an anonymous inner class?

Inside a lambda, 'this' refers to the enclosing instance (lexical scoping, same as ordinary code in that method); inside an anonymous inner class, 'this' refers to the anonymous class instance itself, requiring Outer.this to reach the enclosing instance.

What is the difference between a lambda's expression body and block body?

An expression body (-> expr) is a single expression whose value is implicitly returned, with no braces or return keyword; a block body (-> { ... }) can hold multiple statements and needs an explicit return statement if the interface method returns a value.

Can a lambda expression be assigned to a variable of any interface type?

No — only to a functional interface (an interface with exactly one abstract method) whose single method's signature the lambda's parameter list and body are compatible with.

Why can't a lambda declare checked exceptions that its target functional interface's method doesn't declare?

Because the lambda is providing an implementation of that method, and Java's checked-exception rules require an overriding/implementing method to not throw checked exceptions beyond what the interface method signature declares.

What compile-time guarantee does effectively-final enforcement give about a lambda's captured state?

It guarantees the captured variable's value cannot change after the lambda is created, so the lambda always sees a single, stable snapshot of that variable — eliminating race conditions or inconsistent reads if the lambda executes later or concurrently.

Write a program that uses a lambda-backed Comparator to sort a List<String> by string length descending.

import java.util.*; public class SortByLength { public static void main(String[] args) { List<String> words = new ArrayList<>(List.of("a", "ccc", "bb", "dddd")); words.sort((s1, s2) -> s2.length() - s1.length()); System.out.println(words); // [dddd, ccc, bb, a] } }

The lambda (s1, s2) -> s2.length() - s1.length() compares by length in reverse, sorting longest first: [dddd, ccc, bb, a].

Does this code compile, and why or why not?

It does not compile. x is reassigned after being used in the lambda's scope, so it is not effectively final, and the lambda cannot legally capture it — Java requires the compiler to see, statically, that x is assigned exactly once in its scope.

Write a program that stores a lambda (implementing Runnable) in a List<Runnable> for three different messages and then runs each one.

import java.util.*; public class RunnableList { public static void main(String[] args) { List<Runnable> tasks = new ArrayList<>(); String[] messages = {"first", "second", "third"}; for (String msg : messages) { tasks.add(() -> System.out.println("Running: " + msg)); } for (Runnable task : tasks) { task.run(); } } }

Each lambda captures its own effectively-final 'msg' from that iteration of the for-each loop (a fresh binding each time), so each Runnable prints its own correct message: Running: first, Running: second, Running: third.

Supplier<Integer> supplier;
int x = 5;
supplier = () -> x * 2;
x = 10; // would this compile if placed BEFORE the lambda is created and used afterward?
System.out.println(supplier.get());