Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
The SAM Contract, @FunctionalInterface, and Default Methods
// ── @FunctionalInterface — compiler enforcement ────────────────────────
@FunctionalInterface
interface MySupplier<T> {
T get(); // the single abstract method
// Default methods don't violate SAM:
default MySupplier<T> memoize() {
T[] value = (T[]) new Object[1];
boolean[] computed = {false};
return () -> {
if (!computed[0]) { value[0] = get(); computed[0] = true; }
return value[0];
};
}
// Static methods don't violate SAM:
static <T> MySupplier<T> of(T value) {
return () -> value;
}
}
// COMPILE ERROR — two abstract methods:
// @FunctionalInterface
// interface BadInterface {
// void doThis();
// void doThat(); // ERROR: multiple non-overriding abstract methods found
// }
// OK — second method overrides Object.equals (not counted as abstract):
@FunctionalInterface
interface StringTransformer {
String transform(String s);
@Override boolean equals(Object obj); // not counted — from Object
}
// ── Inheritance and SAM ────────────────────────────────────────────────
@FunctionalInterface
interface Base {
void execute();
}
// Child inherits the SAM from Base — still functional:
@FunctionalInterface
interface NamedBase extends Base {
// No new abstract methods — inherits execute() from Base
default String name() { return "NamedBase"; }
}
NamedBase nb = () -> System.out.println("executing"); // lambda for execute()
nb.execute();
// NOT functional — adds a second abstract method:
interface TwoMethods extends Base {
void anotherMethod(); // second abstract method — not functional
}
// ── Standard functional interfaces with default methods ───────────────
// Function.andThen() — compose two functions sequentially:
Function<String, Integer> length = String::length;
Function<Integer, String> intToStr = i -> "Length: " + i;
Function<String, String> composed = length.andThen(intToStr);
System.out.println(composed.apply("hello")); // "Length: 5"
// Function.compose() — compose in reverse order:
Function<String, String> composed2 = intToStr.compose(length);
System.out.println(composed2.apply("world")); // "Length: 5"
// Predicate.and(), or(), negate():
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> longEnough = s -> s.length() >= 3;
Predicate<String> validInput = notEmpty.and(longEnough);
Predicate<String> anyOk = notEmpty.or(longEnough);
Predicate<String> isEmpty = notEmpty.negate();
System.out.println(validInput.test("hi")); // false (too short)
System.out.println(validInput.test("hello")); // true
System.out.println(isEmpty.test("")); // true
// Function.identity() — no-op transformation:
Function<String, String> identity = Function.identity();
System.out.println(identity.apply("unchanged")); // "unchanged"The java.util.function Hierarchy — All 43 Standard Interfaces
// ── The four root types ───────────────────────────────────────────────
// Function<T,R>: T → R
Function<String, Integer> strLen = String::length; // String → int
Function<Integer, String> intStr = Object::toString; // Integer → String
System.out.println(strLen.apply("hello")); // 5
System.out.println(intStr.apply(42)); // "42"
// Consumer<T>: T → void
Consumer<String> printer = System.out::println;
Consumer<String> logger = s -> System.err.println("[LOG] " + s);
Consumer<String> both = printer.andThen(logger); // sequential composition
both.accept("message"); // prints to stdout and stderr
// Supplier<T>: () → T
Supplier<List<String>> listMaker = ArrayList::new;
Supplier<Instant> now = Instant::now;
System.out.println(listMaker.get().getClass().getSimpleName()); // ArrayList
System.out.println(now.get()); // current instant
// Predicate<T>: T → boolean
Predicate<String> blank = String::isBlank;
Predicate<String> longStr = s -> s.length() > 10;
System.out.println(blank.test(" ")); // true
System.out.println(longStr.test("hello")); // false
// ── Binary variants ───────────────────────────────────────────────────
BiFunction<String, Integer, String> repeat = (s, n) ->
s.repeat(n);
System.out.println(repeat.apply("ab", 3)); // "ababab"
BiConsumer<String, Integer> printTimes = (s, n) -> {
for (int i = 0; i < n; i++) System.out.print(s);
System.out.println();
};
printTimes.accept("*", 5); // *****
BiPredicate<String, String> startsWith = String::startsWith;
System.out.println(startsWith.test("hello", "hel")); // true
// ── Operator specializations ──────────────────────────────────────────
UnaryOperator<String> upper = String::toUpperCase;
BinaryOperator<String> concat = String::concat;
BinaryOperator<Integer> max = Integer::max;
System.out.println(upper.apply("hello")); // HELLO
System.out.println(concat.apply("foo", "bar")); // foobar
System.out.println(max.apply(3, 7)); // 7
// BinaryOperator.minBy() and maxBy() — useful for reduce():
List<String> words = List.of("apple", "banana", "cherry", "date");
Optional<String> longest = words.stream()
.reduce(BinaryOperator.maxBy(Comparator.comparingInt(String::length)));
System.out.println(longest.orElse("none")); // banana or cherry (6 chars)
// ── Primitive specializations — avoiding boxing ────────────────────────
// WITHOUT primitive interface — boxes every int:
Function<Integer, Integer> squareBoxed = n -> n * n;
int result1 = squareBoxed.apply(5); // autoboxes 5 → Integer, result → int
// WITH primitive interface — no boxing:
IntUnaryOperator squarePrimitive = n -> n * n;
int result2 = squarePrimitive.applyAsInt(5); // pure int arithmetic
// Benchmark impact for processing arrays:
int[] numbers = IntStream.rangeClosed(1, 1_000_000).toArray();
// With boxing (IntStream → boxed → unboxed):
long sum1 = Arrays.stream(numbers).boxed()
.map(n -> n * n) // Function<Integer,Integer>: boxing
.mapToLong(Integer::longValue)
.sum();
// Without boxing (all primitives):
long sum2 = Arrays.stream(numbers)
.map(n -> n * n) // IntUnaryOperator: no boxing
.asLongStream()
.sum();
// sum2 is significantly faster for large arrays
// ── ObjXxxConsumer — mixed generic+primitive ─────────────────────────
ObjIntConsumer<StringBuilder> appendN = (sb, n) -> sb.append(n);
StringBuilder sb = new StringBuilder();
appendN.accept(sb, 42);
appendN.accept(sb, 7);
System.out.println(sb); // "427"
// Common in streams for indexed operations:
List<String> items = List.of("a", "b", "c");
ObjIntConsumer<String> indexedPrint = (s, i) -> System.out.println(i + ": " + s);
for (int i = 0; i < items.size(); i++) {
indexedPrint.accept(items.get(i), i);
}Custom Functional Interfaces, Checked Exceptions, and Composition Patterns
// ── Custom functional interface — when standard ones don't fit ─────────
// Better semantics than Function<Order, ValidationResult>:
@FunctionalInterface
interface OrderValidator {
ValidationResult validate(Order order);
// Default composition:
default OrderValidator and(OrderValidator other) {
return order -> {
ValidationResult r1 = this.validate(order);
return r1.isValid() ? other.validate(order) : r1;
};
}
default OrderValidator or(OrderValidator other) {
return order -> {
ValidationResult r1 = this.validate(order);
return r1.isValid() ? r1 : other.validate(order);
};
}
}
// Usage reads like English:
OrderValidator notEmpty = order -> order.items().isEmpty()
? ValidationResult.error("Order is empty")
: ValidationResult.ok();
OrderValidator notExpired = order -> order.expiresAt().isBefore(Instant.now())
? ValidationResult.error("Order expired")
: ValidationResult.ok();
OrderValidator combined = notEmpty.and(notExpired);
ValidationResult result = combined.validate(someOrder);
// ── Custom interface with checked exception ────────────────────────────
@FunctionalInterface
interface IOFunction<T, R> {
R apply(T t) throws IOException;
// Adapter to unchecked Function:
default Function<T, R> unchecked() {
return t -> {
try { return apply(t); }
catch (IOException e) { throw new UncheckedIOException(e); }
};
}
// Static factory — converts checked to unchecked at the boundary:
static <T, R> Function<T, R> wrap(IOFunction<T, R> f) {
return f.unchecked();
}
}
// Lambda can throw IOException:
IOFunction<Path, String> reader = Files::readString;
String content = reader.apply(Path.of("file.txt")); // can throw IOException
// Used in stream with unchecked adapter:
List<Path> paths = List.of(Path.of("a.txt"), Path.of("b.txt"));
List<String> contents = paths.stream()
.map(IOFunction.wrap(Files::readString)) // checked → unchecked at boundary
.collect(Collectors.toList());
// ── Pipeline with fluent default methods ──────────────────────────────
@FunctionalInterface
interface Transformer<T> {
T transform(T input);
default Transformer<T> then(Transformer<T> next) {
return input -> next.transform(this.transform(input));
}
static <T> Transformer<T> identity() {
return input -> input;
}
}
Transformer<String> trim = String::trim;
Transformer<String> normalize = s -> s.replaceAll("\s+", " ");
Transformer<String> lower = String::toLowerCase;
Transformer<String> pipeline = trim.then(normalize).then(lower);
System.out.println(pipeline.transform(" Hello World ")); // "hello world"
// ── Converting between similar functional interfaces ───────────────────
// Function<T,Boolean> vs Predicate<T> — logically same, different API
Function<String, Boolean> funcPred = String::isEmpty;
Predicate<String> pred = String::isEmpty;
// Convert Function<T,Boolean> to Predicate<T>:
Predicate<String> fromFunc = funcPred::apply; // method reference adapts
System.out.println(fromFunc.test("")); // true
// Convert Predicate<T> to Function<T,Boolean>:
Function<String, Boolean> fromPred = pred::test;
System.out.println(fromPred.apply("")); // true
// ── Functional interface assignability rules ───────────────────────────
// A lambda can be assigned to ANY functional interface with a matching signature:
Runnable r = () -> System.out.println("hello"); // () → void
Callable<Void> c = () -> { System.out.println("hello"); return null; }; // () → Void
// Same lambda body, different interface types — both legal:
// The compiler checks structural compatibility, not nominal types
Supplier<String> s1 = () -> "hello";
Callable<String> s2 = () -> "hello"; // Callable<V> has V call() — same signature
// s1 and s2 have the same lambda body but different types — not assignable to each other
// No widening — lambda types don't form a hierarchy:
// Supplier<String> s3 = s2; // COMPILE ERROR: Callable is not Supplier
// Cast works only when structurally compatible AND you know the runtime type:
// Generally avoid casting lambdas — use explicit assignment to target type