Lambda Expression

July 5, 20264 min readUpdated 8/20/2026

A lambda is a function you can pass to a method as if it were a value. That is the whole idea, and it works because of one rule: a lambda is shorthand for implementing an interface that has exactly one abstract method. Understand that rule and lambdas stop looking like magic.

Where they came from

Before Java 8, passing behaviour meant an anonymous class. Six lines to say "compare by length":

List<String> names = new ArrayList<>(List.of("Christopher", "Bo", "Ana"));

names.sort(new Comparator<String>() {              // the old way
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

names.sort((a, b) -> a.length() - b.length());     // the same thing
System.out.println(names);                          // [Bo, Ana, Christopher]

Everything the compiler could work out has been removed. It knows sort wants a Comparator<String>, that the single method is compare, and that both parameters are String. All that is left is the part only you know: what the comparison is.

Syntax

Runnable noArgs = () -> System.out.println("running");

Function<String, Integer> oneArg = s -> s.length();       // parentheses optional for one

BiFunction<Integer, Integer, Integer> twoArgs = (a, b) -> a + b;

Function<String, String> block = s -> {                   // braces need an explicit return
    String trimmed = s.strip();
    return trimmed.toUpperCase();
};

BinaryOperator<Integer> typed = (Integer a, Integer b) -> a * b;   // types allowed, rarely needed

Two rules cover the shape. A single expression body returns its value automatically; a braced body needs return. Types are inferred; write them only when the compiler genuinely cannot work them out.

Functional interfaces

A lambda has no type of its own — it takes the type of the functional interface it is assigned to. You could define your own, but java.util.function already has the ones you need:

InterfaceMethodTakes → returnsUsed for
Predicate<T>testT → booleanfiltering, conditions
Function<T,R>applyT → Rtransforming
Consumer<T>acceptT → nothingdoing something with each item
Supplier<T>getnothing → Tproducing a value lazily
UnaryOperator<T>applyT → Ttransforming without changing type
BiFunction<T,U,R>applyT, U → Rcombining two things
Predicate<String> isLong = s -> s.length() > 5;
Function<String, String> shout = s -> s.toUpperCase() + "!";
Consumer<String> print = s -> System.out.println(s);
Supplier<LocalDate> today = () -> LocalDate.now();

System.out.println(isLong.test("hello"));       // false
System.out.println(shout.apply("hey"));         // HEY!
print.accept("done");

These compose, which is where they get genuinely useful:

Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> isShort = s -> s.length() < 10;

Predicate<String> valid = notBlank.and(isShort);
System.out.println(valid.test("hello"));        // true
System.out.println(valid.negate().test(""));    // true

Function<String, String> trim = String::strip;
Function<String, Integer> length = String::length;
System.out.println(trim.andThen(length).apply("  hi  "));   // 2

Writing your own

@FunctionalInterface
interface Discount {
    double apply(double price);          // exactly one abstract method
}

class Checkout {
    double checkout(double price, Discount discount) {
        return discount.apply(price);
    }

    void run() {
        System.out.println(checkout(100, p -> p * 0.9));      // 90.0 — 10% off
        System.out.println(checkout(100, p -> p - 15));       // 85.0 — flat 15 off
        System.out.println(checkout(100, p -> p));            // 100.0 — none
    }
}

Note what this buys: checkout does not know or care what a discount is. Adding a new kind of discount does not touch it. That is the same benefit interfaces gave in Interfaces, with far less ceremony.

Effectively final capture

A lambda can use variables from the enclosing scope, but only if they never change:

void demo() {
    String prefix = "Order ";                        // never reassigned
    List<String> ids = List.of("1", "2");

    ids.forEach(id -> System.out.println(prefix + id));    // fine

    int count = 0;
    // ids.forEach(id -> count++);   // "local variables referenced from a lambda expression
                                     //  must be final or effectively final"
}

The restriction exists because a lambda may outlive the method that created it — it could run on another thread, or minutes later. Copying a value is safe; sharing a variable that has since changed is not.

Note that it restricts reassignment, not mutation. A List you captured can still have items added to it. If you find yourself wanting a counter, that is a sign the work belongs in a stream with count() or a collector.

The primitive variants, and why they exist

Alongside Function and Predicate you will see IntFunction, IntPredicate, ToIntFunction, DoubleSupplier and a dozen more. They are not clutter — they exist to avoid boxing.

Function<Integer, Integer> boxed = n -> n * 2;      // every call boxes and unboxes
IntUnaryOperator unboxed = n -> n * 2;              // works on int directly

System.out.println(boxed.apply(21));                // 42
System.out.println(unboxed.applyAsInt(21));         // 42

ToIntFunction<String> length = String::length;      // String in, primitive int out
System.out.println(length.applyAsInt("hello"));     // 5

Each Integer is an object on the heap. For one call that is irrelevant; over millions of elements in a stream it is measurable. Use the primitive variants when you are working with numbers in bulk, and the general ones everywhere else — do not let this decide the shape of ordinary code.

When a lambda is the wrong choice

Lambdas are for small pieces of behaviour. Three signs you have gone too far:

  • It does not fit on a couple of lines. Extract it to a method and pass a method reference instead — the method gets a name, which is documentation.
  • You need to reuse it. Same answer.
  • It is nested inside another lambda. Almost always harder to read than the loop it replaced.

One practical note: a stack trace from inside a lambda shows a synthetic name such as lambda$run$0, which tells you very little. That alone is a reason to keep them short.

Next

A lambda needs an interface with one abstract method to implement. Functional Interfaces is next — the ones the JDK already gives you, and how they compose.