Built-in Functional Interfaces and Method References
Java provides a standard library of functional interfaces in java.util.function. When writing lambdas, prefer these built-in interfaces over creating your own — they are well-known, well-documented, and compose well together.
Key built-in functional interfaces
Function<T, R>
Represents a function that accepts one argument and produces a result.
Function<String, Integer> lengthFn = s -> s.length();
int len = lengthFn.apply("hello"); // 5
// Compose functions
Function<Integer, String> intToStr = i -> "Length: " + i;
Function<String, String> describeLength = lengthFn.andThen(intToStr);
describeLength.apply("hello"); // "Length: 5"
Predicate
Represents a boolean-valued function of one argument.
Predicate<String> isLong = s -> s.length() > 5;
isLong.test("hello"); // false
isLong.test("hello world"); // true
// Combine predicates
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> longAndStartsWithA = isLong.and(startsWithA);
Consumer
Represents an operation that accepts a single input and returns no result (side-effecting).
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello"); // prints "Hello"
// Chain consumers
Consumer<String> logger = s -> log.debug(s);
Consumer<String> printAndLog = printer.andThen(logger);
Supplier
Represents a supplier of results — no input, produces a value. Used for lazy evaluation.
Supplier<Double> randomSupplier = () -> Math.random();
double value = randomSupplier.get(); // new random number each call
// Lazy default with Optional
String name = maybeName.orElseGet(() -> computeExpensiveFallback());
BiFunction<T, U, R>
Represents a function that accepts two arguments and produces a result.
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
int sum = add.apply(3, 5); // 8
UnaryOperator and BinaryOperator
Specialised forms of Function and BiFunction where all types are the same:
UnaryOperator<String> upper = s -> s.toUpperCase();
BinaryOperator<Integer> multiply = (a, b) -> a * b;
Primitive specialisations
To avoid boxing overhead, there are specialised interfaces for primitives:
| Generic | Int version | Long version | Double version |
|---|---|---|---|
Predicate<T> | IntPredicate | LongPredicate | DoublePredicate |
Consumer<T> | IntConsumer | LongConsumer | DoubleConsumer |
Supplier<T> | IntSupplier | LongSupplier | DoubleSupplier |
Function<T,R> | IntFunction<R> | LongFunction<R> | DoubleFunction<R> |
IntToLongFunction | LongToIntFunction | DoubleToIntFunction | |
ToIntFunction<T> | ToLongFunction<T> | ToDoubleFunction<T> |
IntPredicate isEven = i -> i % 2 == 0;
IntUnaryOperator square = i -> i * i; // int → int
ToIntFunction<String> length = s -> s.length(); // T → int
Method references
When a lambda’s body is simply “call this existing method with the same arguments”, a method reference is a shorter, often more readable equivalent.
Four forms
| Form | Syntax | Equivalent lambda | Example |
|---|---|---|---|
| Static method | Class::staticMethod | (x) -> Class.staticMethod(x) | Math::sqrt |
| Instance method on specific object | instance::instanceMethod | (x) -> instance.instanceMethod(x) | System.out::println |
| Instance method on arbitrary object of type | Class::instanceMethod | (obj, x) -> obj.instanceMethod(x) | String::toUpperCase |
| Constructor | Class::new | () -> new Class() | ArrayList::new |
Examples
// Static method
Function<Double, Double> sqrtFn = Math::sqrt;
// equivalent: d -> Math.sqrt(d)
// Instance method on specific object
Consumer<String> printer = System.out::println;
// equivalent: s -> System.out.println(s)
// Instance method on first argument (the first param becomes the receiver)
Function<String, String> toUpper = String::toUpperCase;
// equivalent: s -> s.toUpperCase()
UnaryOperator<String> toUpperOp = String::toUpperCase;
// same as above but uses UnaryOperator
// Constructor reference
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();
// Constructor with arguments
Function<Integer, ArrayList<String>> sizedFactory = ArrayList::new;
ArrayList<String> sizedList = sizedFactory.apply(50);
Method references vs lambdas
Method references are NOT the same as lambda expressions syntactically, but they produce equivalent functional interface instances. The choice is stylistic:
// All three are equivalent
list.forEach(x -> System.out.println(x));
list.forEach(y -> { System.out.println(y); });
list.forEach(System.out::println);
// Stream pipeline with both
names.stream()
.map(s -> s.toUpperCase()) // lambda
.map(String::toUpperCase) // method reference — equivalent
.forEach(System.out::println); // method reference — equivalent
The Tripos may ask you to rewrite code in different styles — make sure you can go in both directions.
Comparing functional interfaces to custom interfaces
Before java.util.function, every lambda use required a custom functional interface. Now you can use the standard ones wherever they fit the semantic intent:
// Old style: custom interface
interface StringChecker { boolean check(String s); }
// New style: use standard Predicate
Predicate<String> checker = s -> s.length() > 0;
When to create a custom interface: If the method signature is complex, the parameter names carry important semantic meaning, or the @FunctionalInterface annotation documents the interface’s intended role in your API.
Tripos relevance
Understand the common functional interfaces and method references — they appear in stream pipeline questions and in questions asking you to refactor anonymous classes to lambdas. Be able to identify the target functional interface from context and to rewrite a lambda as a method reference (and vice versa).