Java 23: Lambdas & Functional Interfaces
easy⏱ 5 mincoursejava
The functional interface
@FunctionalInterface interface Func { int apply(int x); }. Exactly one abstract method (SAM = Single Abstract Method). default and static methods don't count against the single. The annotation is optional but catches accidents (second abstract method = compile error).
@FunctionalInterface
interface Adder { int add(int a, int b); }
Adder plus = (a, b) -> a + b;
plus.add(2, 3); // 5
The core five
Function<T, R>: R apply(T). Predicate<T>: boolean test(T). Consumer<T>: void accept(T). Supplier<T>: T get(). BiFunction<T, U, R>: two args. Primitive specializations avoid boxing: IntFunction<R>, ToIntFunction<T>, IntPredicate, etc.
Method references
Four kinds: (1) static: Integer::parseInt. (2) bound instance: obj::method. (3) unbound instance: String::length — the receiver becomes the first arg. (4) constructor: ArrayList::new. All produce equivalent lambdas and are often more readable.
Function<String, Integer> len1 = s -> s.length(); // lambda
Function<String, Integer> len2 = String::length; // method ref
Build a Function pipeline
Implement compose and andThen on a tiny Func<A, B> class — f.andThen(g) returns x => g(f(x)), g.compose(f) returns x => g(f(x)) (same result, different read order). Chain 4 functions that trim, uppercase, prefix with '> ', and truncate to 10 chars. Run on 3 inputs.
Lambdas capture effectively final
Lambdas can reference local variables only if they're final or effectively final (never reassigned). This prevents data races in closed-over state and lets the compiler transform lambdas into static call sites. If you need mutation, use an AtomicInteger or a single-element array.
Quiz: Lambda vs anonymous class
What's the main difference? A lambda doesn't create a this — it captures the enclosing this. Anonymous classes create their own this. Lambdas are also compiled to invokedynamic + LambdaMetafactory (no separate class file per lambda).