Topic 11. Functional Interfaces and Lambdas
Functional interfaces and lambdas in Java allow behavior to be passed as values. They complement OOP: domain modeling remains object-oriented, while local behavioral rules are expressed functionally.
Core idea
Before Java 8, simple behavior often required verbose anonymous classes. Lambdas reduced boilerplate and made intent clearer.
A functional interface has a single abstract method (SAM).
@FunctionalInterface
interface MathOp {
int apply(int a, int b);
}
MathOp add = (a, b) -> a + b;
System.out.println(add.apply(2, 3)); // 5
Where functional style is used in Java
- Collections (
sort,removeIf,forEach). - Stream API (
map,filter,reduce). - Async flows (
CompletableFuture). - Callbacks and strategy injection.
- Task execution (
Runnable,Callable).
Standard functional interfaces
Predicate<T>:T -> boolean.Function<T, R>:T -> R.Consumer<T>:T -> void.Supplier<T>:() -> T.UnaryOperator<T>:T -> T.BinaryOperator<T>:(T, T) -> T.BiFunction<T, U, R>:(T, U) -> R.
Example 1. Predicate + Function
Predicate<String> longName = s -> s.length() >= 4;
Function<String, String> upper = String::toUpperCase;
String result = List.of("ann", "alex").stream()
.filter(longName)
.map(upper)
.findFirst()
.orElse("NONE");
System.out.println(result); // ALEX
Example 2. Consumer + Supplier
Supplier<UUID> idSupplier = UUID::randomUUID;
Consumer<UUID> printer = id -> System.out.println("ID=" + id);
UUID id = idSupplier.get();
printer.accept(id);
Lambda syntax patterns
() -> 42
x -> x * 2
(a, b) -> a + b
(String s) -> s.trim()
(a, b) -> {
int r = a + b;
return r;
}
Guideline: shorter lambdas are usually clearer.
Method references
Method references shorten lambdas that only call existing methods.
Forms:
ClassName::staticMethodobj::instanceMethodClassName::instanceMethodClassName::new
List<String> names = new ArrayList<>(List.of("Bob", "Ann", "alex"));
names.sort(String::compareToIgnoreCase);
System.out.println(names); // [alex, Ann, Bob]
Closures and effectively final
Lambdas can capture local variables only if they are final or effectively final.
int min = 3;
Predicate<String> p = s -> s.length() >= min;
Invalid:
int min = 3;
min++; // no longer effectively final
This rule avoids confusing lifetime and concurrency behavior.
Function composition
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> pipeline = trim.andThen(upper);
System.out.println(pipeline.apply(" java ")); // JAVA
For predicates:
Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> shortWord = s -> s.length() <= 5;
Predicate<String> rule = notBlank.and(shortWord);
System.out.println(rule.test("java")); // true
Practical patterns
Strategy injection with lambda
interface DiscountStrategy {
double apply(double amount);
}
double checkout(double amount, DiscountStrategy strategy) {
return strategy.apply(amount);
}
double result = checkout(1000, a -> a * 0.9);
Reusable callback wrapper
void withLogging(String opName, Runnable action) {
long start = System.nanoTime();
try {
action.run();
} finally {
System.out.println(opName + " took " + (System.nanoTime() - start));
}
}
Common mistakes
- very long lambdas with complex business logic,
- side effects hidden inside stream operations,
- unclear lambda parameter names in complex contexts,
- creating custom functional interfaces when standard ones fit.
When a regular method is better
Extract named methods when:
- logic is reused across places,
- lambda is too long,
- logic needs separate focused tests.
Key takeaway
Functional interfaces and lambdas improve expressiveness when used with discipline: small focused lambdas, clear contracts, minimal side effects.