Lambda Expressions and Functional Interfaces in Java
Part 1 of 9 in Functional Java Unleashed
Java 8 introduced lambda expressions as a way to write anonymous functions inline — a terse syntax for implementing functional interfaces (interfaces with exactly one abstract method). Before lambdas, you reached for anonymous inner classes; with them, a block of logic becomes a single expression that flows through your code.
This post walks through four interconnected concepts: behavior parameterization (why lambdas exist), the four standard functional interfaces from java.util.function, method references (::) as a shorthand for named methods, and Java’s type inference rules that keep the syntax clean.
Behavior Parameterization
Behavior parameterization is the idea of letting a method accept how something should be done as an argument. Without lambdas, this meant anonymous inner classes; with them, the behavior becomes an inline expression passed directly to the function.
static <T> List<T> filter(List<T> items, Predicate<T> pred) {
List<T> result = new ArrayList<>();
for (T item : items) {
if (pred.test(item)) result.add(item);
}
return result;
}
// Anonymous inner class — 5 lines of boilerplate
Predicate<Apple> redFilter = new Predicate<Apple>() {
@Override public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
};
// Lambda — same behavior, one line
Predicate<Apple> redLambda = apple -> "red".equals(apple.getColor());
The first half of the run shows the anonymous inner class producing two red apples (200g and 90g). The second half shows an identical result from a lambda — same behavior, zero ceremony. But the real value appears in the bottom section: one filter() method reused with three completely different behaviors passed as lambdas.
The mental model is simple: a lambda is an unnamed method whose parameter and return types are inferred from context. You write only the body; the surrounding type tells Java the rest.
The Four Standard Functional Interfaces
java.util.function ships with dozens of functional interfaces, but four cover almost every practical case:
Predicate<T>—T → boolean. Use for filtering, validation, decisions.Consumer<T>—T → void. Use for side effects: printing, saving, mutating.Function<T, R>—T → R. Use for transformation and mapping.Supplier<T>—() → T. Use for lazy creation where the value is computed only when needed.
// Predicate — filtering red apples
Predicate<Apple> redFilter = apple -> "red".equals(apple.getColor());
inventory.stream().filter(redFilter).forEach(System.out::println);
// Consumer — printing each green apple's weight
Consumer<Apple> printGreenWeight = apple -> {
if ("green".equals(apple.getColor()))
System.out.println(apple.getWeight() + "g");
};
inventory.forEach(printGreenWeight);
// Function — mapping apples to their weight
Function<Apple, Integer> getWeight = Apple::getWeight;
System.out.println(getWeight.apply(new Apple("red", 150))); // prints: 150
// Supplier — lazy creation
Supplier<Apple> fresh = () -> new Apple("green", 120);
The run covers all four interfaces in sequence. Notice the Function section includes .andThen() chaining — two functions composed together (getWeight.andThen(kgFormatter)) produce 0.15 kg from a 150g apple, showing that functional interfaces compose cleanly.
Method References
A method reference is a lambda that simply delegates to an existing named method. Java recognizes four patterns:
// 1. Static: ClassName::staticMethod
Comparator<Apple> byWeight = ComparatorHelper::compareByWeightDesc;
// 2. Particular obj: objRef::instanceMethod
String text = "pineapple";
Predicate<String> hasPine = text::contains; // word -> text.contains(word)
// 3. Arbitrary obj: ClassName::instanceMethod (on each argument)
Comparator<String> ci = String::compareToIgnoreCase;
Function<String, String> upper = String::toUpperCase;
// 4. Constructor: ClassName::new
BiFunction<String, Integer, Apple> maker = Apple::new;
Supplier<Apple> lazy = () -> new Apple("red", 150);
Section 1 sorts apples by weight descending (the static method compareByWeightDesc replaces a two-parameter lambda). Section 2 shows how text::contains lifts the receiver into the function — it becomes a Predicate<String> that checks whether each input is contained in “pineapple”. Sections 3 and 4 show arbitrary-object method references and constructors.
The key insight: method references are syntactic sugar. The compiler expands String::toUpperCase to s -> s.toUpperCase() behind the scenes — there’s no runtime difference.
Type Inference
Java’s lambda type inference saves you from writing parameter types repeatedly. The compiler infers types from three places:
- The variable declaration —
Predicate<Apple> p = apple -> ...tells Java the parameter is an Apple. - The surrounding generic method —
filter(fruits, f -> f.length() > 5)infersT = StringbecausefruitsisList<String>and thePredicate<T>target narrowsTto String. - Nested lambdas — types propagate through layers:
target -> word -> word.contains(target)works without annotations on both levels.
Predicate<Apple> heavy = apple -> apple.getWeight() >= 150;
// 'apple' is Apple (from Predicate<Apple>)
Function<String, Predicate<String>> contains = target -> word -> word.contains(target);
// target is String (outer Function's parameter)
// word is String (inner Predicate's parameter) — inferred from the same context
Consumer<Apple> showLabel = apple -> {
String label = "Medium"; // no annotation needed
if (apple.getWeight() >= 150) label = "Heavy";
System.out.println(label);
};
The run demonstrates inference at work in six situations. Section 6 is worth attention: a lambda a -> a.getColor() and the method reference Apple::getColor produce identical output because both resolve to the same underlying call — the compiler infers Function<Apple, String> from the variable type in both cases.
Section 7 shows the limit of inference. Without a declared target type (var x = ...), the compiler has nothing to infer from: it doesn’t know whether x -> x.getWeight() returns an int (for Function) or a boolean (for Predicate). The variable declaration does the heavy lifting.
Takeaway
A lambda is just an unnamed method — write only the body, and Java infers the types from context. Method references (::) are shorthand for lambdas that delegate to existing code, and the four standard interfaces (Predicate, Consumer, Function, Supplier) cover most use cases. Together they turn verbose anonymous classes into readable expressions that pass behavior as a first-class value.