12.3 Lambda Expressions and Functional Interfaces
What Is a Lambda Expression?
Introduced in Java 8, lambda expressions are anonymous functions — they let you treat code as data, passing behavior as a method argument. They dramatically reduce boilerplate and make Java feel modern.
// Before lambdas: anonymous inner class
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Running!");
}
};
// With lambda: same thing, one line
Runnable r = () -> System.out.println("Running!");
1. Lambda Syntax
// () -> expression — no params, expression body
// (x) -> expression — one param, expression body
// x -> expression — one param, parens optional
// (x, y) -> expression — two params
// (x, y) -> { statements; } — block body with statements
// Examples
Runnable r = () -> System.out.println("Hello");
Supplier<String> s = () -> "Hello World";
Consumer<String> c = name -> System.out.println("Hi " + name);
Function<Integer, Integer> f = x -> x * 2;
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
// Block body for multiple statements
Function<String, String> process = input -> {
String trimmed = input.trim();
return trimmed.toUpperCase();
};
2. Functional Interfaces
A functional interface has exactly one abstract method. This is the type a lambda is assigned to. The @FunctionalInterface annotation enforces this.
@FunctionalInterface
interface Greeter {
String greet(String name); // one abstract method
// default and static methods are allowed
}
Greeter formal = name -> "Dear " + name + ",";
Greeter casual = name -> "Hey, " + name + "!";
Greeter shouting = name -> ("Hello " + name).toUpperCase();
System.out.println(formal.greet("Alice")); // Dear Alice,
System.out.println(casual.greet("Bob")); // Hey, Bob!
System.out.println(shouting.greet("Carol")); // HELLO CAROL
3. Built-in Functional Interfaces (java.util.function)
Java provides a rich set of ready-to-use functional interfaces:
Predicate<T> — test a condition, returns boolean
import java.util.function.Predicate;
import java.util.List;
Predicate<String> isLong = s -> s.length() > 5;
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> notEmpty = s -> !s.isEmpty();
System.out.println(isLong.test("Hello")); // false
System.out.println(isLong.test("Hello World")); // true
System.out.println(isEven.test(4)); // true
// Combining predicates
Predicate<String> isLongAndNotEmpty = isLong.and(notEmpty);
Predicate<String> isLongOrShort = isLong.or(s -> s.length() < 3);
Predicate<String> isShort = isLong.negate();
List<String> words = List.of("hi", "hello", "Java", "programming", "code");
words.stream()
.filter(isLong)
.forEach(System.out::println); // hello, programming
// BiPredicate for two arguments
java.util.function.BiPredicate<String, Integer> longerThan = (s, n) -> s.length() > n;
System.out.println(longerThan.test("hello", 3)); // true
Function<T, R> — transform T into R
import java.util.function.Function;
Function<String, Integer> strToLen = s -> s.length();
Function<Integer, String> intToStr = n -> "Number: " + n;
Function<String, String> toUpper = String::toUpperCase;
System.out.println(strToLen.apply("hello")); // 5
System.out.println(intToStr.apply(42)); // Number: 42
// Composing functions
Function<String, String> trim = String::trim;
Function<String, Integer> trimAndLen = trim.andThen(strToLen);
// or: trim.andThen(strToLen) is equivalent to: s -> strToLen.apply(trim.apply(s))
System.out.println(trimAndLen.apply(" hello ")); // 5
// compose() applies the argument function FIRST
Function<Integer, Integer> timesTwo = x -> x * 2;
Function<Integer, Integer> plusThree = x -> x + 3;
Function<Integer, Integer> timesTwoThenPlusThree = plusThree.compose(timesTwo); // timesTwo first
Function<Integer, Integer> plusThreeThenTimesTwo = timesTwo.andThen(plusThree); // plusThree second? no:
// andThen: apply timesTwo, then plusThree
// compose: apply timesTwo (the argument) first, then plusThree (the caller)
System.out.println(timesTwoThenPlusThree.apply(5)); // (5*2)+3 = 13
System.out.println(plusThreeThenTimesTwo.apply(5)); // (5*2)+3 = 13 (same here)
// Actually: timesTwo.andThen(plusThree): first *2, then +3 → (5*2)+3=13
// plusThree.compose(timesTwo): first *2, then +3 → (5*2)+3=13
Consumer<T> — consume T, no return value
import java.util.function.Consumer;
Consumer<String> print = System.out::println;
Consumer<String> log = s -> System.out.println("[LOG] " + s);
print.accept("Hello"); // Hello
log.accept("Error occurred"); // [LOG] Error occurred
// andThen: chain consumers
Consumer<String> printAndLog = print.andThen(log);
printAndLog.accept("Test");
// Hello
// [LOG] Test
// BiConsumer for two arguments
java.util.function.BiConsumer<String, Integer> printScore = (name, score) ->
System.out.printf("%-10s: %d%n", name, score);
printScore.accept("Alice", 95);
Supplier<T> — produce T, no input
import java.util.function.Supplier;
Supplier<String> hello = () -> "Hello World";
Supplier<Integer> randomNum = () -> (int) (Math.random() * 100);
Supplier<java.util.List<String>> emptyList = java.util.ArrayList::new;
System.out.println(hello.get()); // Hello World
System.out.println(randomNum.get()); // e.g., 42
System.out.println(emptyList.get()); // []
// Lazy initialization pattern
class HeavyObject {
HeavyObject() { System.out.println("Expensive construction!"); }
}
Supplier<HeavyObject> lazy = HeavyObject::new;
// Construction is deferred until .get() is called
HeavyObject obj = lazy.get(); // "Expensive construction!" printed here
4. Primitive Functional Interfaces
To avoid boxing overhead, Java provides specialized interfaces for primitives:
import java.util.function.*;
// IntPredicate, LongPredicate, DoublePredicate
IntPredicate isPositive = n -> n > 0;
System.out.println(isPositive.test(5)); // true
// IntFunction<R>, IntUnaryOperator, IntBinaryOperator
IntUnaryOperator square = n -> n * n;
IntBinaryOperator multiply = (a, b) -> a * b;
System.out.println(square.applyAsInt(7)); // 49
System.out.println(multiply.applyAsInt(3, 4)); // 12
// IntSupplier, IntConsumer
IntSupplier counter = new IntSupplier() {
int count = 0;
public int getAsInt() { return count++; }
};
IntConsumer printInt = n -> System.out.print(n + " ");
for (int i = 0; i < 5; i++) printInt.accept(counter.getAsInt()); // 0 1 2 3 4
5. Lambda Variable Capture
Lambdas can capture variables from their enclosing scope:
// Capture of effectively final local variable
String prefix = "Hello, "; // effectively final (never reassigned)
Function<String, String> greeter = name -> prefix + name;
System.out.println(greeter.apply("Alice")); // Hello, Alice
// prefix = "Hi, "; // uncommenting this would cause a compile error
// Instance fields and static fields can be captured and mutated
class Counter {
private int count = 0;
public Runnable incrementer() {
return () -> count++; // captures 'this', not a local variable
}
}
Counter c = new Counter();
Runnable inc = c.incrementer();
inc.run(); inc.run(); inc.run();
// c.count is now 3
6. Practical Example: Sorting with Lambdas
import java.util.*;
import java.util.function.*;
record Employee(String name, String department, double salary) {}
public class LambdaSortingDemo {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>(List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 72000),
new Employee("Charlie", "Engineering", 88000),
new Employee("Diana", "HR", 65000),
new Employee("Eve", "Marketing", 78000)
));
// Sort by salary descending
employees.sort((e1, e2) -> Double.compare(e2.salary(), e1.salary()));
employees.forEach(e -> System.out.printf("%-10s $%.0f%n", e.name(), e.salary()));
System.out.println("---");
// Sort by department, then by name within same department
employees.sort(Comparator.comparing(Employee::department)
.thenComparing(Employee::name));
employees.forEach(e -> System.out.printf("%-12s %-10s%n", e.department(), e.name()));
// Filter using Predicate
Predicate<Employee> isHighEarner = e -> e.salary() > 80000;
Predicate<Employee> isEngineer = e -> e.department().equals("Engineering");
System.out.println("\nHigh earners in Engineering:");
employees.stream()
.filter(isHighEarner.and(isEngineer))
.map(Employee::name)
.forEach(System.out::println);
// Transform using Function
Function<Employee, String> summary = e ->
String.format("%s (%s) - $%.0f", e.name(), e.department(), e.salary());
System.out.println("\nEmployee summaries:");
employees.stream()
.map(summary)
.forEach(System.out::println);
}
}
Lambda best practices:
-
Keep lambdas short— if a lambda is more than 2-3 lines, extract it to a named method and use a method reference.
-
Avoid side effects in stream lambdas — lambdas should ideally be pure functions (no modifying external state).
-
Use method references when the lambda just calls an existing method:
.map(s -> s.toUpperCase()) // lambda
.map(String::toUpperCase) // method reference (prefer this) -
Prefer specific functional interfaces: Use
IntFunction,IntPredicate, etc. overFunction<Integer, ...>to avoid boxing/unboxing overhead in performance-critical code. -
Don't use lambdas everywhere— regular loops and methods are sometimes clearer. Use lambdas when they genuinely reduce noise and increase clarity.