Mastering Java Lambda Expressions and Functional Interfaces
Part 11 of 14 in Functional Java Unleashed
Before Java 8, passing code as data meant writing anonymous inner classes — five to ten lines of boilerplate just to express a simple condition. Lambda expressions collapse that down to a single expression. Functional interfaces give the JVM a type for “a function with this signature,” and together they unlock behavior parameterization: write your algorithm once, plug in the logic it needs.
This post walks through three focused examples. Each one isolates a different facet of lambdas in Java: the syntax simplification, reusable behavior via functional interfaces, and composing operations into pipelines.
Lambda syntax — what you save
Implementing a one-method interface used to require an anonymous class with @Override, type declarations, and braces. A lambda is the same thing without the ceremony. The JVM infers the parameter types from the target functional interface:
Both the anonymous class and the lambda produce identical output. The lambda version replaces five lines of boilerplate with one expression. With single-parameter lambdas, even the parentheses around the parameter are optional — name -> "Hi, " + name is enough.
The four functional interfaces you’ll use most
Java ships java.util.function with built-in interfaces for the common shapes. Here’s what each one does:
Predicate
var engineers = employees.stream()
.filter(e -> e.department().equals("Engineering"))
.toList();
This returned 3 employees: Alice (105K), and Frank ($110K).
Function<T, R> takes a value and returns a transformed value. Use it for mapping:
var names = employees.stream()
.map(Employee::name)
.toList();
// [Alice, Bob, Carol, Dave, Eve, Frank]
Employee::name is a method reference — syntactic sugar for e -> e.name() when the lambda body calls exactly one existing method with matching signature. Formatted salaries showed $95,000 through $110,000.
Consumer
employees.forEach(e ->
System.out.printf("%s earns %,.2f/month%n", e.name(), e.salary() / 12.0));
Alice’s 7,916.67` per month. The lambda runs once per element.
Supplier
Supplier<List<Employee>> highEarners = () ->
employees.stream().filter(e -> e.salary() > 90000).toList();
var selected = highEarners.get();
This produced 4 high earners (salary strictly above 95K), Carol (91K), and Frank ($110K). The code doesn’t execute until .get() is called.
Behavior parameterization — one method, many behaviors
The real power of lambdas shows up when you write a generic method that accepts behavior as a parameter, then plug in different logic at each call site:
Before (one method per criterion):
List<Employee> filterBySalary(List<Employee> employees, int min) { ... }
List<Employee> filterByDept(List<Employee> employees, String dept) { ... }
Each method repeats the same for loop. Add a new criterion and you add a new method.
After (one method, arbitrary behavior):
List<Employee> filter(List<Employee> employees, Predicate<Employee> pred) {
var result = new ArrayList<Employee>();
for (var e : employees)
if (pred.test(e)) result.add(e);
return result;
}
The same method produces different results depending on the lambda passed in: e -> e.salary() >= 90000 gave 4 results, e -> e.department().equals("Engineering") gave 3, and the compound e -> e.salary() > 80000 && !e.department().equals("Finance") also gave 4 (Alice 82K, Carol 110K).
Anonymous classes and lambdas are fully interchangeable here — the last test in the output confirmed both approaches returned 3 results for the same condition. A reusable Predicate<Employee> variable can be stored once and applied across different data sets without rewriting logic.
Composing operations — filter, map, sorted
Lambdas shine when you chain stream operations. Each step is a lambda (or method reference) that receives the output of the previous step:
Step by step:
filter(e -> department == "Engineering")→ 3 engineers (Alice, Carol, Frank)filter(e -> salary >= 90K)→ same 3 (all three earn above $90K)map(Employee::name)→[Alice, Bob, Carol, Dave, Eve, Frank]sorted(salary).reversed().limit(3)→ Frank (105K), Alice ($95K)
A composed pipeline combining all operations produced the same three senior engineers in descending salary order. No temporary collections, no nested loops — each lambda is a standalone step in a pipeline. The Collections.reverseOrder() call on the mapped strings gave alphabetical descending output (Frank, Carol, Alice).
Takeaway
A functional interface is a contract: “I expect one method with this signature.” A lambda is the inline implementation of that contract. With built-in interfaces like Predicate, Function, Consumer, and Supplier, you write the algorithm once (the stream pipeline) and plug in arbitrary logic through lambdas. The behavior parameterization pattern — generic method, specific lambda at call site — eliminates boilerplate while keeping each operation focused on a single concern.