Skip to content
Part IA Michaelmas Term

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:

GenericInt versionLong versionDouble version
Predicate<T>IntPredicateLongPredicateDoublePredicate
Consumer<T>IntConsumerLongConsumerDoubleConsumer
Supplier<T>IntSupplierLongSupplierDoubleSupplier
Function<T,R>IntFunction<R>LongFunction<R>DoubleFunction<R>
IntToLongFunctionLongToIntFunctionDoubleToIntFunction
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

FormSyntaxEquivalent lambdaExample
Static methodClass::staticMethod(x) -> Class.staticMethod(x)Math::sqrt
Instance method on specific objectinstance::instanceMethod(x) -> instance.instanceMethod(x)System.out::println
Instance method on arbitrary object of typeClass::instanceMethod(obj, x) -> obj.instanceMethod(x)String::toUpperCase
ConstructorClass::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).