Guide contents

Lesson 13 of 71

Lambdas and Functional Interfaces

A lambda expression (Java 8) is a block of code passed as a value. Its type is always a functional interface: an interface with exactly one abstract method. This lesson covers the standard functional interfaces, lambda and method reference syntax, which variables a lambda may use, how this differs from anonymous classes, composition, and when evaluating a lambda allocates an object.

Topic API or rule
Functional interface one abstract method, optional @FunctionalInterface
Standard families Function, Supplier, Consumer, Predicate, BiFunction, UnaryOperator, BinaryOperator
Primitive specializations IntFunction, ToIntFunction, IntPredicate, IntUnaryOperator, IntBinaryOperator, ...
Method references Type::staticMethod, obj::method, Type::instanceMethod, Type::new
Capture local variables must be final or effectively final
Composition andThen, compose, Predicate.and, or, negate

Example 1: a functional interface and a lambda

An interface with one abstract method can be implemented by a lambda. default and static methods do not count toward the one abstract method, and neither do methods that Object already declares (such as equals). @FunctionalInterface makes the compiler reject the interface if a second abstract method is added.

Java
public class FirstLambda {
    @FunctionalInterface
    interface PriceRule {
        long apply(long cents);                       // the single abstract method

        default PriceRule twice() { return c -> apply(apply(c)); }
        boolean equals(Object o);                     // declared by Object, not counted
    }

    public static void main(String[] args) {
        PriceRule tenPercentOff = cents -> cents - cents / 10;
        PriceRule flatFee = cents -> cents + 50;

        System.out.println("10% off 1000: " + tenPercentOff.apply(1000));
        System.out.println("fee on 1000: " + flatFee.apply(1000));
        System.out.println("10% off twice: " + tenPercentOff.twice().apply(1000));
    }
}
Output
10% off 1000: 900
fee on 1000: 1050
10% off twice: 810

The lambda cents -> cents - cents / 10 takes its parameter type (long) and return type from PriceRule.apply. The type the compiler expects at that position is called the target type; the same lambda text can have different types in different contexts.

Example 2: lambda syntax forms

A lambda has a parameter list, an arrow, and a body. The body is an expression (its value is returned) or a block with explicit return. Parameter types may be omitted, written explicitly, or written as var (Java 11), but all parameters use the same style.

Java
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

public class Syntax {
    public static void main(String[] args) {
        Supplier<String> none = () -> "no parameters";
        Function<String, Integer> one = s -> s.length();              // parentheses optional
        BiFunction<Integer, Integer, Integer> typed = (Integer a, Integer b) -> a + b;
        BiFunction<Integer, Integer, Integer> withVar = (var a, var b) -> a * b;
        Function<Integer, String> block = n -> {
            if (n < 0) return "negative";
            return "non-negative";
        };

        System.out.println(none.get());
        System.out.println(one.apply("lambda"));
        System.out.println(typed.apply(2, 3));
        System.out.println(withVar.apply(2, 3));
        System.out.println(block.apply(-5));
    }
}
Output
no parameters
6
5
6
negative

Example 3: the java.util.function families

java.util.function defines 43 interfaces. Most belong to a few families, named by what goes in and what comes out.

Interface Method In, out
Supplier<T> get() nothing, T
Consumer<T> accept(T) T, nothing
Predicate<T> test(T) T, boolean
Function<T, R> apply(T) T, R
UnaryOperator<T> apply(T) T, T (extends Function<T, T>)
BiFunction<T, U, R> apply(T, U) two values, R
BinaryOperator<T> apply(T, T) two T, T

The generic interfaces work only with reference types, so an int passes through as a boxed Integer. Primitive specializations avoid that boxing: IntPredicate.test(int), IntUnaryOperator.applyAsInt(int), ToIntFunction<T>.applyAsInt(T), IntFunction<R>.apply(int), and the same for long and double. Boxing is covered in Types and Variables.

Java
import java.util.ArrayList;
import java.util.List;
import java.util.function.*;

public class Families {
    public static void main(String[] args) {
        Supplier<List<String>> newList = () -> new ArrayList<>();
        Consumer<List<String>> addTwo = l -> { l.add("a"); l.add("bb"); };
        Predicate<String> longer = s -> s.length() > 1;
        Function<String, Integer> len = String::length;
        UnaryOperator<String> upper = s -> s.toUpperCase();
        BinaryOperator<Integer> max = (a, b) -> a >= b ? a : b;

        List<String> list = newList.get();
        addTwo.accept(list);
        System.out.println(list + " longer[1]=" + longer.test(list.get(1)));
        System.out.println("len=" + len.apply("abc") + " upper=" + upper.apply("abc")
                + " max=" + max.apply(3, 7));

        IntPredicate even = n -> n % 2 == 0;            // no Integer objects
        IntUnaryOperator square = n -> n * n;
        IntBinaryOperator sum = (a, b) -> a + b;
        ToIntFunction<String> length = String::length;
        int total = 0;
        for (int i = 1; i <= 4; i++) {
            if (even.test(i)) total = sum.applyAsInt(total, square.applyAsInt(i));
        }
        System.out.println("sum of even squares 1..4 = " + total);
        System.out.println("length via ToIntFunction = " + length.applyAsInt("hello"));
    }
}
Output
[a, bb] longer[1]=true
len=3 upper=ABC max=7
sum of even squares 1..4 = 20
length via ToIntFunction = 5

Example 4: the four kinds of method references

A method reference is a short form of a lambda that only calls one existing method or constructor.

Kind Form Equivalent lambda
Static method Integer::parseInt s -> Integer.parseInt(s)
Bound instance method prefix::concat s -> prefix.concat(s)
Unbound instance method String::toUpperCase s -> s.toUpperCase()
Constructor StringBuilder::new s -> new StringBuilder(s)

In the unbound form, the first parameter of the functional method becomes the receiver. A bound reference evaluates its receiver once, when the reference is created.

Java
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

public class MethodRefs {
    public static void main(String[] args) {
        Function<String, Integer> parse = Integer::parseInt;              // static
        String prefix = "id-";
        Function<String, String> addPrefix = prefix::concat;              // bound
        Function<String, String> upper = String::toUpperCase;             // unbound, 1 arg
        BiFunction<String, String, Boolean> starts = String::startsWith;  // unbound, 2 args
        Function<String, StringBuilder> builder = StringBuilder::new;     // constructor
        Supplier<StringBuilder> empty = StringBuilder::new;               // other constructor

        System.out.println(parse.apply("42") + 1);
        System.out.println(addPrefix.apply("7"));
        System.out.println(upper.apply("ref"));
        System.out.println(starts.apply("lambda", "lam"));
        System.out.println(builder.apply("abc").reverse());
        System.out.println("empty length " + empty.get().length());
    }
}
Output
43
id-7
REF
true
cba
empty length 0

StringBuilder::new matches two different constructors because the target type decides which overload is used: Function<String, StringBuilder> selects StringBuilder(String), Supplier<StringBuilder> selects StringBuilder().

Example 5: capture and effectively final

A lambda may read local variables of the enclosing method only if they are final or effectively final (never assigned after initialization). The lambda receives a copy of the value, so the rule prevents the copy and the variable from disagreeing. Fields and array elements have no such rule, because the lambda reads them through a reference.

Java
import java.util.function.IntSupplier;
import java.util.function.Supplier;

public class Capture {
    private int counter;                       // a field: lambdas may change it

    void run() {
        int base = 100;                        // effectively final
        IntSupplier plusBase = () -> base + 1;
        // base++;                             // would make "base" not effectively final:
        //                                        compile error in the lambda above
        Supplier<Integer> next = () -> ++counter;   // captures this, not a copy of counter
        next.get();
        next.get();

        int[] box = {0};                       // the array reference is effectively final
        Runnable inc = () -> box[0]++;
        inc.run();
        inc.run();
        inc.run();

        System.out.println("plusBase=" + plusBase.getAsInt());
        System.out.println("counter=" + counter + " box[0]=" + box[0]);
    }

    public static void main(String[] args) {
        new Capture().run();
    }
}
Output
plusBase=101
counter=2 box[0]=3

The one-element array is a known workaround, but it is not thread safe: two threads running inc can lose updates. Atomics and CAS covers the thread-safe counter.

The next program shows the compile error when a captured variable is reassigned.

Java✗ does not compile
import java.util.function.IntSupplier;

public class NotEffectivelyFinal {
    public static void main(String[] args) {
        int base = 100;
        base = 200;
        IntSupplier s = () -> base;   // error: local variables referenced from a lambda
                                      // expression must be final or effectively final
        System.out.println(s.getAsInt());
    }
}

Example 6: this in lambdas vs anonymous classes

An anonymous class is a new class, so this inside it refers to the anonymous object, and its own members shadow the enclosing ones. A lambda is not a new scope for names: this and toString() refer to the enclosing instance, and a lambda parameter may not reuse the name of a local variable.

Java
public class ThisDemo {
    private final String name = "outer";

    @Override public String toString() { return "ThisDemo(" + name + ")"; }

    void show() {
        Runnable anonymous = new Runnable() {
            @Override public String toString() { return "anonymous object"; }
            @Override public void run() { System.out.println("anonymous this: " + this); }
        };
        Runnable lambda = () -> System.out.println("lambda this:    " + this);

        anonymous.run();
        lambda.run();
        System.out.println("lambda is a ThisDemo: " + ((Object) lambda instanceof ThisDemo));
    }

    public static void main(String[] args) {
        new ThisDemo().show();
    }
}
Output
anonymous this: anonymous object
lambda this:    ThisDemo(outer)
lambda is a ThisDemo: false

The lambda object itself is not a ThisDemo; only the name this inside its body resolves to the enclosing object. A lambda that uses this (or an instance field) captures the enclosing instance and keeps it reachable for as long as the lambda lives.

Example 7: composition

Function.andThen(g) applies this function, then g. Function.compose(g) applies g first. Predicate has and, or, negate, and the static Predicate.not (Java 11). Consumer.andThen runs two consumers in order. Each call returns a new functional object, so a rule set can be built from small parts.

Java
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

public class Compose {
    public static void main(String[] args) {
        Function<Integer, Integer> plus2 = x -> x + 2;
        Function<Integer, Integer> times3 = x -> x * 3;
        System.out.println("plus2.andThen(times3)(1) = " + plus2.andThen(times3).apply(1));
        System.out.println("plus2.compose(times3)(1) = " + plus2.compose(times3).apply(1));

        Predicate<String> nonEmpty = Predicate.not(String::isEmpty);
        Predicate<String> shortWord = s -> s.length() <= 3;
        Predicate<String> accepted = nonEmpty.and(shortWord.negate()).or(s -> s.equals("ok"));

        StringBuilder log = new StringBuilder();
        Consumer<String> print = s -> log.append(s);
        Consumer<String> sep = s -> log.append(';');
        Consumer<String> both = print.andThen(sep);

        for (String s : List.of("", "ok", "abc", "trade", "order")) {
            if (accepted.test(s)) both.accept(s);
        }
        System.out.println("accepted: " + log);
    }
}
Output
plus2.andThen(times3)(1) = 9
plus2.compose(times3)(1) = 5
accepted: ok;trade;order;

Example 8: capturing vs non-capturing lambdas and allocation

The compiler turns a lambda body into a private method and emits an invokedynamic instruction that produces the functional object at run time; the instruction is covered in Bytecode and Class Loading. The language specification allows an implementation to reuse lambda objects. On HotSpot a non-capturing lambda (one that uses no local variables and no this) evaluates to the same object every time, so it does not allocate after the first use. A capturing lambda stores the captured values in a new object on each evaluation.

Java
import java.util.function.IntSupplier;
import java.util.function.IntUnaryOperator;

public class Allocation {
    static IntUnaryOperator nonCapturing() { return x -> x + 1; }
    static IntSupplier capturing(int value) { return () -> value; }

    public static void main(String[] args) {
        IntUnaryOperator a = nonCapturing();
        IntUnaryOperator b = nonCapturing();
        IntSupplier c = capturing(5);
        IntSupplier d = capturing(5);

        System.out.println("non-capturing same object: " + (a == b));
        System.out.println("capturing same object:     " + (c == d));
        System.out.println("results: " + a.applyAsInt(1) + " " + c.getAsInt() + " " + d.getAsInt());
        System.out.println("lambda class is synthetic: " + a.getClass().isSynthetic());
        System.out.println("lambda class is hidden:    " + a.getClass().isHidden());
    }
}
Output
non-capturing same object: true
capturing same object:     false
results: 2 5 5
lambda class is synthetic: true
lambda class is hidden:    true

The lambda classes are hidden classes (Java 15), generated at run time and not visible by name to other code. Programs must not depend on identity of lambda objects: == between them is unspecified by the language. On a hot path, a capturing lambda inside a loop can allocate on each iteration; the JIT may remove that allocation (escape analysis is covered in a later section), but code that must not allocate keeps its lambdas non-capturing or stores them in fields.

Interview points

  • A functional interface has exactly one abstract method; default, static, and Object methods do not count. @FunctionalInterface makes the rule a compile check.
  • A lambda has no type of its own; the target type decides it, which is why StringBuilder::new can match different constructors.
  • Four method reference kinds: static, bound instance (obj::m), unbound instance (Type::m, first argument is the receiver), constructor (Type::new).
  • Captured locals must be final or effectively final because the lambda copies the value.
  • In a lambda, this is the enclosing instance; in an anonymous class, it is the anonymous object.
  • Use IntPredicate, ToIntFunction, IntUnaryOperator and the other primitive specializations to avoid boxing.
  • f.andThen(g) is g(f(x)); f.compose(g) is f(g(x)).
  • Lambdas compile to a private method plus invokedynamic; non-capturing lambdas are reused on HotSpot, capturing lambdas allocate per evaluation.

Summary

  • Lambdas and method references are values whose type is a functional interface.
  • java.util.function covers suppliers, consumers, predicates, functions, and operators, with primitive versions for int, long, and double.
  • A lambda body shares the enclosing scope for names, including this.
  • Composition methods return new functional objects built from existing ones.
  • Lambda classes are hidden classes generated at run time; object identity of lambdas is not specified.