Functional Interfaces
IntermediateMaster the java.util.function package — Predicate, Function, Consumer, Supplier, and their Bi and primitive variants used throughout the Streams API.
Overview
A functional interface has exactly one abstract method (SAM). The java.util.function package ships 43 ready-made functional interfaces covering every combination of input/output. Knowing the four core types and their composition methods is essential for working with Streams, Optional, CompletableFuture, and any functional-style Java code.
The Four Core Types
Predicate<T> — T → boolean (test) Function<T,R> — T → R (apply) Consumer<T> — T → void (accept) Supplier<T> — () → T (get)
Each has a Bi variant for two inputs: BiPredicate<T,U>, BiFunction<T,U,R>, BiConsumer<T,U>. UnaryOperator<T> extends Function<T,T>. BinaryOperator<T> extends BiFunction<T,T,T>.
import java.util.function.*;
import java.util.List;
public class CoreFunctional {
public static void main(String[] args) {
// Predicate<T> — test a condition
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> startsA = s -> s.startsWith("A");
Predicate<String> both = isLong.and(startsA);
Predicate<String> either = isLong.or(startsA);
Predicate<String> notLong = isLong.negate();
System.out.println(both.test("Avocado")); // true
System.out.println(both.test("Ant")); // false
// Function<T,R> — transform a value
Function<String, Integer> length = String::length;
Function<String, String> upper = String::toUpperCase;
Function<String, String> pipeline = upper.andThen(s -> s + "!");
System.out.println(pipeline.apply("hello")); // HELLO!
// compose: g.compose(f) = g(f(x)); andThen: f.andThen(g) = g(f(x))
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus3 = x -> x + 3;
System.out.println(times2.andThen(plus3).apply(5)); // 13 = (5*2)+3
System.out.println(times2.compose(plus3).apply(5)); // 16 = (5+3)*2
// Consumer<T> — side effect, returns void
Consumer<String> logger = s -> System.out.println("[LOG] " + s);
Consumer<String> saver = s -> System.out.println("[SAVE] " + s);
Consumer<String> both2 = logger.andThen(saver);
both2.accept("event");
// Supplier<T> — produce a value
Supplier<List<String>> listFactory = java.util.ArrayList::new;
List<String> l = listFactory.get();
l.add("item");
System.out.println(l);
}
}Primitive Specialisations & Custom Interfaces
Boxing/unboxing overhead matters in tight loops. The JDK provides primitive-specialised interfaces to avoid it:
IntPredicate, LongPredicate, DoublePredicate IntFunction<R>, IntUnaryOperator, IntBinaryOperator ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> IntConsumer, LongConsumer, DoubleConsumer IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier
Always use these over Predicate<Integer> in performance-critical paths.
import java.util.function.*;
@FunctionalInterface
interface ThrowingSupplier<T> {
T get() throws Exception;
// Static factory to wrap checked exceptions
static <T> Supplier<T> unchecked(ThrowingSupplier<T> s) {
return () -> {
try { return s.get(); }
catch (Exception e) { throw new RuntimeException(e); }
};
}
}
public class PrimitiveAndCustom {
public static void main(String[] args) {
// Primitive specialisation — no boxing
IntPredicate isEven = n -> n % 2 == 0;
IntUnaryOperator doubler = n -> n * 2;
IntBinaryOperator add = Integer::sum;
System.out.println(isEven.test(4)); // true
System.out.println(doubler.applyAsInt(5)); // 10
System.out.println(add.applyAsInt(3, 7)); // 10
// ToIntFunction — object → primitive
ToIntFunction<String> length = String::length;
System.out.println(length.applyAsInt("hello")); // 5
// Custom functional interface handling checked exceptions
Supplier<String> reader = ThrowingSupplier.unchecked(
() -> new java.io.BufferedReader(
new java.io.StringReader("test")).readLine());
System.out.println(reader.get()); // test
}
}Method References as Functional Interfaces
Any method reference (::) can be assigned to a matching functional interface. This is the bridge between OOP methods and functional programming. The four types of method references map to these functional interface shapes:
ClassName::staticMethod → Function/Consumer/Supplier instance::instanceMethod → Function/Consumer/Supplier ClassName::instanceMethod (unbound) → BiFunction where first arg is receiver ClassName::new (constructor) → Supplier/Function
import java.util.function.*;
import java.util.List;
import java.util.stream.Collectors;
public class MethodRefAsFunctional {
static int doubleIt(int n) { return n * 2; }
int triple(int n) { return n * 3; }
public static void main(String[] args) {
MethodRefAsFunctional obj = new MethodRefAsFunctional();
// Static method reference
IntUnaryOperator d = MethodRefAsFunctional::doubleIt;
System.out.println(d.applyAsInt(5)); // 10
// Instance method reference (bound)
IntUnaryOperator t = obj::triple;
System.out.println(t.applyAsInt(5)); // 15
// Unbound instance method reference — first arg is the receiver
Function<String, String> upper = String::toUpperCase;
BiFunction<String, String, Boolean> startsWith = String::startsWith;
System.out.println(upper.apply("hello")); // HELLO
System.out.println(startsWith.apply("hello", "he")); // true
// Constructor reference
Supplier<List<String>> listNew = java.util.ArrayList::new;
Function<String, StringBuilder> sbNew = StringBuilder::new;
System.out.println(sbNew.apply("Java").append(" rocks")); // Java rocks
// Practical: collect using constructor reference
List<String> words = List.of("one", "two", "three");
List<String> upper2 = words.stream()
.map(String::toUpperCase) // unbound method ref
.collect(Collectors.toList());
System.out.println(upper2); // [ONE, TWO, THREE]
}
}Interactive Visualization
stream.filter(n → n%2==0).map(n → n*n).sorted().collect(toList())Key Points to Remember
- Four core types: Predicate (test), Function (transform), Consumer (side-effect), Supplier (produce)
- Composition: Predicate.and/or/negate; Function.andThen/compose; Consumer.andThen
- Use primitive specialisations (IntPredicate, ToIntFunction…) to avoid boxing overhead
- BiXxx variants accept two inputs; UnaryOperator/BinaryOperator are same-type Function specialisations
- @FunctionalInterface is optional but enables compile-time enforcement of the SAM constraint
- Method references (::) are syntactic sugar for lambdas implementing matching functional interfaces
Practice Functional Interfaces in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the difference between Predicate, Function, Consumer, and Supplier?
What does Function.compose() do versus Function.andThen()?
What are primitive functional interfaces and why do they exist?
How many abstract methods can a functional interface have?
What is the difference between UnaryOperator and Function?
Ask Aria about Functional Interfaces
Your personal AI tutor — ask anything about this concept