Lambdas, functional interfaces
Functional interfaces are contracts, not just syntax targets
A functional interface is an interface with exactly one abstract method. Lambdas are instances of such interfaces. The key is not the lambda syntax — it is the behavioral contract defined by the interface.
Built-in functional interfaces
- Function<T,R>
- Consumer<T>
- Supplier<T>
- Predicate<T>
- BiFunction, BiConsumer, etc.
Prefer standard interfaces instead of creating custom ones unless domain semantics require it.
Lambda capture semantics
Lambdas can capture variables from surrounding scope, but only if they are effectively final.
int threshold = 10;
list.stream()
.filter(x -> x > threshold)
.toList();
The variable threshold must not be modified after assignment.
Why effectively final exists
It prevents confusing shared mutable state between outer scope and lambda execution.
Side effects inside lambdas
Side effects reduce clarity and introduce hidden coupling.
List<String> result = new ArrayList<>(); list.forEach(x -> result.add(x.toUpperCase())); // side effect
Prefer pure transformations:
List<String> result = list.stream()
.map(String::toUpperCase)
.toList();
Closure pitfalls in concurrency
Captured variables can create subtle concurrency bugs if the lambda runs asynchronously.
for (int i = 0; i < 10; i++) {
executor.submit(() -> System.out.println(i)); // compile error unless effectively final
}
The effectively-final rule prevents many of these issues.
Method references vs lambdas
Method references improve readability when the lambda only forwards arguments.
list.stream()
.map(String::trim)
.filter(String::isEmpty);
Prefer method references when they improve clarity.
Checked exception problem
Most functional interfaces do not allow checked exceptions. This leads developers to wrap exceptions in RuntimeException.
list.forEach(x -> {
try {
process(x);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
Production implication:
- Loss of original semantic meaning
- Harder retry logic
Safer pattern: wrap at boundary
Keep lambdas simple and propagate checked exceptions in outer layers rather than hiding them deeply in functional chains.
Designing your own functional interface
Use custom functional interfaces when domain semantics matter.
@FunctionalInterface
public interface PricingStrategy {
Money calculate(Order order);
}
This communicates intent better than Function<Order, Money> in domain code.
Statelessness is preferred
Lambdas should ideally be stateless and side-effect free. Stateless functions are easier to test and reason about.
Debuggability considerations
Stack traces in complex stream chains can be harder to read. Keep lambda bodies short and meaningful.
Performance considerations
- Captured variables may create synthetic classes.
- Heavy lambda allocation in tight loops can increase GC pressure.
- Prefer primitive specializations (IntFunction, LongConsumer) to avoid boxing.
Common production pitfalls
- Embedding complex logic inside lambda bodies.
- Swallowing checked exceptions.
- Mixing side effects with stream transformations.
- Using lambdas where a simple loop is clearer.
Checklist
- Prefer built-in functional interfaces when possible.
- Keep lambdas short and side-effect free.
- Understand effectively-final capture rules.
- Do not hide checked exceptions carelessly.
- Use domain-specific functional interfaces when semantics matter.
Final principle
Lambdas improve expressiveness, not complexity tolerance. Use them to clarify intent, not to compress logic.