Lambdas & Functional Interfaces
Java 8 brought functional programming features to the language. Lambdas and functional interfaces are the cornerstone of the Stream API, CompletableFuture, and many modern APIs.
Lambda Syntax
A lambda expression is an anonymous function:
(parameters) -> expression
(parameters) -> { statements; }Examples:
() -> 42— no parameters, returns 42x -> x * 2— single parameter (parens optional), returns double(a, b) -> a + b— two parameters(String s) -> { System.out.println(s); }— explicit type, block body
Functional Interfaces
A functional interface has exactly one abstract method. It can be annotated with @FunctionalInterface (optional but recommended — the compiler enforces the single-abstract-method rule).
Key interfaces from java.util.function:
| Interface | Signature | Description |
|---|---|---|
Predicate<T> | boolean test(T t) | Returns true/false |
Function<T,R> | R apply(T t) | Transforms T to R |
Consumer<T> | void accept(T t) | Consumes T, no return |
Supplier<T> | T get() | Produces T with no input |
BiFunction<T,U,R> | R apply(T t, U u) | Two inputs, one output |
UnaryOperator<T> | T apply(T t) | Function where T == R |
BinaryOperator<T> | T apply(T t1, T t2) | BiFunction where all types == T |
Predicate provides default methods and(), or(), negate() for composing predicates.
Method References
A shorter syntax for lambdas that just delegate to an existing method:
| Kind | Syntax | Equivalent lambda |
|---|---|---|
| Static method | Math::abs | x -> Math.abs(x) |
| Instance method (bound) | str::toUpperCase | () -> str.toUpperCase() |
| Instance method (unbound) | String::toLowerCase | s -> s.toLowerCase() |
| Constructor | ArrayList::new | () -> new ArrayList<>() |
Comparator with Lambda
Lambdas shine for sorting: list.sort((a, b) -> a.length() - b.length()) or the more readable Comparator.comparing(String::length).
Code Examples
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class LambdaSortDemo {
record Person(String name, int age) {}
public static void main(String[] args) {
List<String> words = Arrays.asList("banana", "apple", "kiwi", "cherry", "fig");
// Anonymous class (old style)
words.sort(new Comparator<String>() {
@Override public int compare(String a, String b) {
return a.length() - b.length();
}
});
System.out.println("Anon class : " + words);
// Lambda — same behavior, much shorter
words.sort((a, b) -> a.length() - b.length());
System.out.println("Lambda : " + words);
// Comparator.comparing — most readable
words.sort(Comparator.comparing(String::length));
System.out.println("Comparing : " + words);
// Chained: sort by length, then alphabetically
words.sort(Comparator.comparing(String::length)
.thenComparing(Comparator.naturalOrder()));
System.out.println("Chained : " + words);
// Sort records
List<Person> people = Arrays.asList(
new Person("Charlie", 35),
new Person("Alice", 28),
new Person("Bob", 28)
);
people.sort(Comparator.comparingInt(Person::age)
.thenComparing(Person::name));
people.forEach(p -> System.out.println(p.name() + " " + p.age()));
}
}Comparator.comparing() with a method reference is the most readable sorting idiom. thenComparing() chains secondary sort criteria. Records (Java 16+) provide compact accessor methods like name() and age().
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class FunctionalInterfacesDemo {
public static void main(String[] args) {
// --- Predicate ---
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
Predicate<Integer> isOdd = isEven.negate();
System.out.println("isEven(4) : " + isEven.test(4));
System.out.println("isPositive(-3) : " + isPositive.test(-3));
System.out.println("evenAndPositive(6) : " + isEvenAndPositive.test(6));
System.out.println("evenAndPositive(-4) : " + isEvenAndPositive.test(-4));
System.out.println("isOdd(5) : " + isOdd.test(5));
// --- Function ---
Function<String, Integer> strLen = String::length;
Function<Integer, String> intToStr = Object::toString;
Function<String, String> lenStr = strLen.andThen(intToStr);
System.out.println("strLen("hello") : " + strLen.apply("hello"));
System.out.println("lenStr("world") : " + lenStr.apply("world"));
// --- Consumer ---
Consumer<String> printer = System.out::println;
Consumer<String> upperPrinter = s -> System.out.println(s.toUpperCase());
Consumer<String> both = printer.andThen(upperPrinter);
both.accept("java");
// --- Supplier ---
Supplier<List<String>> listFactory = () -> new java.util.ArrayList<>(Arrays.asList("x", "y"));
System.out.println("Supplier : " + listFactory.get());
}
}Predicate's and/or/negate compose boolean conditions without if-chains. Function.andThen() pipelines transformations. Consumer.andThen() lets you apply two side-effects sequentially.
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
public class MethodRefDemo {
static int doubleIt(int n) { return n * 2; }
String greet(String prefix) { return prefix + " " + this; }
@Override public String toString() { return "MethodRefDemo"; }
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "WORLD", "Java");
// 1. Static method reference: ClassName::staticMethod
Function<Integer, Integer> doubler = MethodRefDemo::doubleIt;
System.out.println("Static ref : " + doubler.apply(5));
// 2. Bound instance method: instance::method (instance is captured)
String prefix = ">>>";
UnaryOperator<String> addPrefix = s -> prefix + s;
words.stream().map(addPrefix).forEach(System.out::println);
// 3. Unbound instance method: ClassName::instanceMethod
// First lambda parameter becomes the receiver
Function<String, String> toLower = String::toLowerCase;
words.stream().map(toLower).forEach(System.out::println);
// 4. Constructor reference: ClassName::new
Supplier<java.util.ArrayList<String>> listMaker = java.util.ArrayList::new;
java.util.ArrayList<String> newList = listMaker.get();
newList.add("constructed");
System.out.println("Constructor : " + newList);
// BiFunction with unbound instance method
BiFunction<String, String, String> concat = String::concat;
System.out.println("BiFunction : " + concat.apply("Hello, ", "World!"));
}
}The four method reference kinds cover all common delegation patterns. Unbound references treat the first lambda parameter as the receiver object, which is why String::toLowerCase fits Function<String,String>.
Quick Quiz
1. Which functional interface from java.util.function takes no input and returns a value?
2. What is the output of `Predicate<Integer> p = n -> n > 0; System.out.println(p.negate().test(5));`?
3. Which method reference syntax represents an unbound instance method reference?
4. What annotation signals that an interface is intended to be a functional interface?
Was this lesson helpful?