Reading — step 1 of 7
Learn
Java 8+ uses functional interfaces — single-abstract-method (SAM) interfaces — as the type for lambdas and method references. Effective Java Items 42–45 cover this in depth.
What's a functional interface?
An interface with EXACTLY ONE abstract method. The @FunctionalInterface annotation enforces it:
@FunctionalInterface
interface Predicate<T> {
boolean test(T t);
}
Predicate<String> isEmpty = s -> s.isEmpty();
isEmpty.test("hi"); // false
Default methods don't count as "abstract," so Comparator<T> (with compare) is functional even though it has many default methods.
The standard functional interfaces (java.util.function)
Learn these — they're everywhere in stream operations and modern APIs:
Function<T, R> // T -> R (apply)
BiFunction<T, U, R> // (T, U) -> R
Predicate<T> // T -> boolean (test)
Consumer<T> // T -> void (accept)
Supplier<T> // () -> T (get)
UnaryOperator<T> // T -> T (apply) - extends Function<T, T>
BinaryOperator<T> // (T, T) -> T (apply)
// Primitive specializations to avoid boxing:
IntFunction<R>, ToIntFunction<T>, IntPredicate, IntConsumer, IntSupplier
The primitive variants matter for performance — Stream<Integer> boxes; IntStream does not.
Lambda syntax
x -> x + 1 // single param, no parens
(x) -> x + 1 // optional parens
(x, y) -> x + y // multi-param
() -> System.out.println("hi") // no params
(int x, int y) -> x + y // explicit types (rare)
(x, y) -> { // block body
int sum = x + y;
return sum * 2;
}
Lambdas can capture effectively final local variables — no var = ... mutation after the lambda is defined.
Method references — the :: operator
For cases where the lambda just calls one method:
// Static method ref
Function<String, Integer> parse = Integer::parseInt;
// equivalent to: s -> Integer.parseInt(s)
// Instance method ref on a specific instance
List<String> list = new ArrayList<>();
Consumer<String> add = list::add;
// Instance method ref on an arbitrary instance
Function<String, Integer> len = String::length;
// equivalent to: s -> s.length()
// Constructor ref
Supplier<ArrayList<String>> factory = ArrayList::new;
Method references are usually clearer than the equivalent lambda. Modern IDEs suggest the conversion.
Composition
Function has helpful default methods:
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus1 = x -> x + 1;
Function<Integer, Integer> times2ThenPlus1 = times2.andThen(plus1);
Function<Integer, Integer> plus1ThenTimes2 = times2.compose(plus1);
times2ThenPlus1.apply(5); // 11 (5*2 + 1)
plus1ThenTimes2.apply(5); // 12 ((5+1) * 2)
Predicate has and, or, negate. Comparator has thenComparing, reversed.
When to use lambdas vs method references vs anonymous classes
Lambdas — for short logic, often single-expression
Method references — when the lambda just calls one method (often clearer)
Anonymous classes — when you need state, multiple methods, or this to refer to the anonymous instance (lambdas inherit this from the enclosing scope)
Common mistakes
- Capturing mutable state — won't compile if not effectively final.
- Anonymous class when lambda would do — verbose, especially for callbacks.
- Lambda when method reference is clearer —
s -> s.toUpperCase()is uglier thanString::toUpperCase. - Capturing
this— keeps the enclosing object alive longer than needed (memory leaks in long-running tasks). - Confusing Function vs UnaryOperator — UnaryOperator is just
Function<T, T>with constraints. Use it when input and output types match.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…