Functional Interfaces: Predicate, Function, Supplier, Consumer, and More
What Is a Functional Interface?
A functional interface is an interface with exactly one abstract method (SAM — Single Abstract Method). This is the type the compiler targets when you write a lambda expression.
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); // the single abstract method
// default and static methods are allowed
}
The @FunctionalInterface annotation is optional but highly recommended — it makes the intent explicit and causes the compiler to fail if you accidentally add a second abstract method.
All existing single-abstract-method interfaces from the JDK work with lambdas automatically:
Runnable r = () -> System.out.println("run");
Callable<Integer> c = () -> 42;
Comparator<String> cmp = (a, b) -> a.compareTo(b);
The java.util.function Package
Java 8 introduced java.util.function with 43 functional interfaces covering every common function shape. They fall into five categories.
Predicate<T>: T → boolean
Tests a condition on a value. Used for filtering.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Hello")); // false
System.out.println(isLong.test("Welcome")); // true
// Stream filtering
List<String> longNames = names.stream()
.filter(isLong)
.collect(Collectors.toList());
Composition methods:
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> isLong = s -> s.length() > 5;
// AND
Predicate<String> longAndA = startsWithA.and(isLong);
// OR
Predicate<String> longOrA = startsWithA.or(isLong);
// NOT
Predicate<String> doesNotStartWithA = startsWithA.negate();
// Static helper — isEqual
Predicate<String> isAlice = Predicate.isEqual("Alice");
Primitive specialisations: IntPredicate, LongPredicate, DoublePredicate — avoid boxing overhead in numeric pipelines.
Function<T, R>: T → R
Transforms a value of type T into a value of type R. Used for mapping.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Function<String, Integer> length = String::length;
Function<String, String> upper = String::toUpperCase;
System.out.println(length.apply("Hello")); // 5
System.out.println(upper.apply("Hello")); // HELLO
// Stream mapping
List<Integer> lengths = names.stream()
.map(length)
.collect(Collectors.toList());
Composition methods:
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, String> toStr = x -> "Value: " + x;
// andThen: apply times2 first, then toStr
Function<Integer, String> times2ThenStr = times2.andThen(toStr);
System.out.println(times2ThenStr.apply(5)); // "Value: 10"
// compose: apply toStr of the inner function to the input, then apply times2
// i.e., compose(g) = this(g(x))
Function<Integer, Integer> strLenThenTimes2 =
times2.compose((String s) -> s.length()); // times2(s.length())
identity() — returns a function that returns its input:
Function<String, String> identity = Function.identity();
// equivalent to: s -> s
Primitive specialisations: IntFunction<R>, ToIntFunction<T>, IntToLongFunction, etc.
Consumer<T>: T → void
Performs a side-effecting operation on a value without returning anything.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Consumer<String> print = System.out::println;
Consumer<String> log = s -> logger.info("Processed: {}", s);
// Chaining with andThen
Consumer<String> printAndLog = print.andThen(log);
printAndLog.accept("Alice"); // prints, then logs
// Used in forEach
names.forEach(print);
map.forEach((k, v) -> System.out.println(k + "=" + v));
Variants: BiConsumer<T,U>, IntConsumer, LongConsumer, DoubleConsumer, ObjIntConsumer<T>.
Supplier<T>: () → T
Produces a value without taking any input. Used for lazy initialisation and factory patterns.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier<LocalDate> today = LocalDate::now;
Supplier<List<String>> newList = ArrayList::new;
System.out.println(today.get()); // current date, evaluated lazily
// Optional.orElseGet uses Supplier for lazy fallback
Optional<User> user = findUser(id);
User result = user.orElseGet(() -> createDefaultUser());
// vs orElse — the default is always computed even if optional is present
User result2 = user.orElse(createDefaultUser()); // createDefaultUser() always called!
The orElse vs orElseGet distinction is one of the most common Java 8 bugs. Always prefer orElseGet when the fallback is expensive to compute.
BiFunction<T, U, R>: (T, U) → R
Two-argument version of Function.
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n); // Java 11+
// Java 8 equivalent:
BiFunction<String, Integer, String> repeatJ8 = (s, n) -> {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) sb.append(s);
return sb.toString();
};
System.out.println(repeatJ8.apply("ab", 3)); // "ababab"
// Map.replaceAll uses BiFunction
map.replaceAll((key, value) -> value.toUpperCase());
UnaryOperator<T>: T → T
Specialisation of Function<T,T> where input and output are the same type. Used when transforming a value in-place.
UnaryOperator<String> trim = String::trim;
UnaryOperator<Integer> increment = n -> n + 1;
// List.replaceAll
list.replaceAll(String::toUpperCase);
// Chain with andThen / compose (inherited from Function)
UnaryOperator<String> trimAndUpper = trim.andThen(String::toUpperCase)::apply;
// Note: andThen returns Function, need ::apply to get back UnaryOperator behaviour
BinaryOperator<T>: (T, T) → T
Two-argument version of UnaryOperator. Used for reduction operations.
BinaryOperator<Integer> add = (a, b) -> a + b;
BinaryOperator<String> concat = String::concat;
// Stream.reduce
int sum = numbers.stream().reduce(0, add);
String combined = words.stream().reduce("", concat);
// Static helpers
BinaryOperator<Integer> maxOp = BinaryOperator.maxBy(Comparator.naturalOrder());
BinaryOperator<Integer> minOp = BinaryOperator.minBy(Comparator.naturalOrder());
Full Reference Table
| Interface | Signature | Primitive variants |
|---|---|---|
Predicate<T> | T → boolean | IntPredicate, LongPredicate, DoublePredicate |
BiPredicate<T,U> | (T,U) → boolean | — |
Function<T,R> | T → R | IntFunction<R>, ToIntFunction<T>, IntToLongFunction, … |
BiFunction<T,U,R> | (T,U) → R | ToIntBiFunction<T,U>, … |
UnaryOperator<T> | T → T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator<T> | (T,T) → T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
Consumer<T> | T → void | IntConsumer, LongConsumer, DoubleConsumer |
BiConsumer<T,U> | (T,U) → void | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> |
Supplier<T> | () → T | IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier |
Defining Your Own Functional Interface
The built-in interfaces cover most cases, but sometimes you need a domain-specific signature:
@FunctionalInterface
public interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
// Helper to wrap as standard Function
static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> f) {
return t -> {
try { return f.apply(t); }
catch (Exception e) { throw new RuntimeException(e); }
};
}
}
// Usage
List<String> contents = paths.stream()
.map(ThrowingFunction.wrap(Files::readString))
.collect(Collectors.toList());
Custom functional interfaces are useful for:
- Adding checked exception support
- Domain-specific naming (
Validator<T>,Transformer<T, R>) - API contracts that communicate intent more clearly than
Function
Practical Patterns
Validation Pipeline
public class Validator<T> {
private final List<Predicate<T>> rules = new ArrayList<>();
public Validator<T> addRule(Predicate<T> rule) {
rules.add(rule);
return this;
}
public boolean validate(T value) {
return rules.stream().allMatch(rule -> rule.test(value));
}
}
Validator<String> passwordValidator = new Validator<String>()
.addRule(s -> s.length() >= 8)
.addRule(s -> s.chars().anyMatch(Character::isDigit))
.addRule(s -> s.chars().anyMatch(Character::isUpperCase));
System.out.println(passwordValidator.validate("Hello123")); // true
System.out.println(passwordValidator.validate("short")); // false
Factory Map
Map<String, Supplier<Animal>> animalFactory = new HashMap<>();
animalFactory.put("dog", Dog::new);
animalFactory.put("cat", Cat::new);
animalFactory.put("bird", Bird::new);
Animal pet = animalFactory
.getOrDefault("dog", () -> { throw new IllegalArgumentException("Unknown"); })
.get();
Transform Pipeline
Function<String, String> pipeline = ((Function<String, String>) String::trim)
.andThen(String::toLowerCase)
.andThen(s -> s.replaceAll("\\s+", "-"));
System.out.println(pipeline.apply(" Hello World ")); // "hello-world"
Summary
| Interface | When to use |
|---|---|
Predicate<T> | Filtering, conditional checks, validation |
Function<T,R> | Mapping, transforming, converting |
Consumer<T> | Side effects: logging, writing, printing |
Supplier<T> | Lazy initialisation, factories, deferred values |
BiFunction<T,U,R> | Two-input transformations |
UnaryOperator<T> | In-place transformation (same type in and out) |
BinaryOperator<T> | Reduction, combining two values of the same type |
Next Step
Method References: Four Kinds and When to Use Each →
Part of the DevOps Monk Java tutorial series: Java 8 → Java 11 → Java 17 → Java 21