☕ Java

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

The single-abstract-method (SAM) constraint is what makes an interface a functional interface. An interface has exactly one abstract method when all non-abstract methods are either default methods (providing an implementation in the interface), static methods (not abstract), or methods that override public methods from Object (like equals, hashCode, toString — these don't count as abstract even if declared abstract in the interface, because they will be implemented by any concrete class inheriting from Object). @FunctionalInterface is a marker annotation that instructs the compiler to verify the SAM constraint. Without it, any interface that happens to have one abstract method is still a valid functional interface and can be used as a lambda target — @FunctionalInterface is purely a documentation and verification mechanism. With it, the compiler produces an error if the interface has zero or more than one abstract method, preventing accidental violations of the contract. All standard library functional interfaces use @FunctionalInterface. Default methods do not violate the SAM constraint. A functional interface may have many default methods providing composed or specialized operations, as long as it has exactly one abstract method. The java.util.function interfaces make heavy use of this: Function<T,R> has one abstract method apply(T t) and default methods andThen(Function<R,V>), compose(Function<V,T>), and static identity(). Predicate<T> has one abstract method test(T t) and default methods and(Predicate), or(Predicate), negate(), and static isEqual(). Static methods in an interface are also compatible with the SAM constraint. An interface with one abstract method and many static factory/utility methods is still a valid functional interface. The distinction between default and static methods in functional interfaces is: default methods define compositions on instances of the interface (you call them on a Predicate instance to produce another Predicate); static methods are utilities not tied to any instance (Predicate.isEqual(obj) returns a Predicate without requiring an existing Predicate). The inheritance rules for functional interfaces follow the same SAM logic. If interface A extends interface B, and both declare abstract methods, the total count of abstract methods is what matters. If A inherits one abstract method from B and declares another, A has two abstract methods and is not a functional interface. If A inherits one abstract method from B and overrides it (not adding another), A has one abstract method and is functional. If A inherits one abstract method from B and narrows its return type (covariant override), the narrowed version has one abstract method and is functional.
Java
// ── @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 java.util.function package provides 43 standard functional interfaces organized by four properties: the number of parameters (nullary, unary, binary), the presence of a return value, the type of inputs and outputs (generic vs primitive), and whether inputs and outputs are the same type. The four root types govern the design: Function<T,R> takes one argument and returns a result; Consumer<T> takes one argument and returns nothing; Supplier<T> takes no arguments and returns a result; Predicate<T> takes one argument and returns boolean. The Binary prefix adds a second argument of the same type as the first: BiFunction<T,U,R>, BiConsumer<T,U>, BiPredicate<T,U>. There is no BiSupplier because a supplier takes no arguments by definition. The Unary and Binary Operator specializations handle the case where input and output types are the same: UnaryOperator<T> extends Function<T,T> (takes T, returns T), BinaryOperator<T> extends BiFunction<T,T,T> (takes two T, returns T). These are used for operations that transform values within the same type — String::toUpperCase as UnaryOperator<String>, Integer::sum as BinaryOperator<Integer>. Primitive specializations exist for int, long, and double (not byte, short, float — these use widening). IntFunction<R> (int → R), LongFunction<R>, DoubleFunction<R>. ToIntFunction<T> (T → int), ToLongFunction<T>, ToDoubleFunction<T>. IntToLongFunction (int → long), IntToDoubleFunction, LongToIntFunction, LongToDoubleFunction, DoubleToIntFunction, DoubleToLongFunction. IntConsumer, LongConsumer, DoubleConsumer. IntSupplier, LongSupplier, DoubleSupplier. IntPredicate, LongPredicate, DoublePredicate. IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator. IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator. ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>. The ObjXxxConsumer specializations handle the mixed case where one argument is generic and one is primitive: ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>. These arise naturally in collectors and fold operations. Primitive interfaces exist to avoid boxing and unboxing overhead. When a stream contains int values and maps to int values, using IntUnaryOperator instead of Function<Integer, Integer> avoids boxing 32-bit ints into Integer objects — which costs object allocation and cache pressure. For performance-sensitive code processing numeric data, always use primitive functional interfaces.
Java
// ── 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

The standard java.util.function interfaces cover most use cases, but custom functional interfaces are appropriate when the signature does not match any standard interface, when checked exceptions must be declared, when the business meaning should be encoded in the interface name, or when additional default methods for domain-specific composition are needed. A custom functional interface is simply an interface annotated @FunctionalInterface with one abstract method. The name is its documentation: Transformer<T> communicates that it transforms values, where Function<T,T> communicates nothing specific. OrderValidator<T extends Order> communicates that it validates orders, where Predicate<T> does not. The checked exception problem with standard interfaces is one of the most common motivations for custom interfaces. Function<T,R> cannot throw IOException — adding a checked exception to a lambda that implements Function<T,R> is a compile error. A custom CheckedFunction<T,R> or IOFunction<T,R> that declares the checked exception allows lambdas to propagate it naturally. The trade-off is that the custom interface doesn't integrate with standard library APIs (stream methods accept Function, not CheckedFunction) without adapter methods. Adapter patterns for converting between checked and unchecked interfaces are common in production code. The wrap() pattern (static <T,R> Function<T,R> wrap(CheckedFunction<T,R> f)) converts a checked-throwing function to an unchecked-throwing Function. Libraries like Vavr provide CheckedFunction0 through CheckedFunction8 with built-in try/unchecked conversion. This pattern should be applied at the boundary where checked exceptions are converted to unchecked, not scattered throughout the codebase. Functional interface composition can be implemented as default methods to create domain-specific DSLs. A Validator<T> that combines validations with and() and or() default methods creates a clear API for composing validation rules. A Pipeline<T> that chains transformations with then() creates a readable processing pipeline. These patterns emerge naturally when the functional interface's domain has obvious composition semantics.
Java
// ── 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

Related Topics in Java 8 Features

Intermediate Operations
Intermediate operations are Stream API methods that transform one stream into another stream, enabling pipeline construction through method chaining. Every intermediate operation is lazy — it does not process any elements when called; it only builds a description of the computation to be performed. The actual processing occurs only when a terminal operation triggers stream traversal, at which point all intermediate operations execute in a single pass over the data, interleaved element by element rather than stage by stage. This laziness and fusion model is the defining architectural feature of the Streams API, distinguishing it from eager collection-transformation approaches. Intermediate operations fall into categories: filtering (filter, distinct), transformation (map and its primitive variants, flatMap), ordering (sorted), size-limiting (limit, skip), peeking (peek), and the Java 9 additions for prefix-based selection (takeWhile, dropWhile). This entry covers the laziness model and why it matters, every intermediate operation with its exact semantics and performance characteristics, stateless versus stateful intermediate operations, short-circuiting operations and how they interact with infinite streams, the map/flatMap distinction, and operation ordering and its performance implications.
Terminal Operations
Terminal operations are Stream API methods that trigger the actual traversal and processing of a stream pipeline, producing a non-stream result — a value, a collection, a side effect, or nothing (void). A stream can have exactly one terminal operation; once invoked, the stream is consumed and cannot be reused (calling any further operation on it throws IllegalStateException). Terminal operations fall into categories: reduction (reduce, collect), matching (anyMatch, allMatch, noneMatch), finding (findFirst, findAny), counting (count), iteration with side effects (forEach, forEachOrdered), and array/collection materialization (toArray, toList). Some terminal operations are short-circuiting (anyMatch, findFirst) and can terminate before examining the entire stream; others (forEach, count in most cases, collect) must examine every element. This entry covers every terminal operation in depth, the reduce() method and its three overloads with the combiner function's role in parallel execution, the Collectors framework as the primary mechanism for complex terminal reduction, short-circuiting semantics and their interaction with infinite streams, the single-use constraint and its rationale, and the choice between equivalent terminal operations for performance and clarity.
Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.
Default Methods
Default methods, introduced in Java 8, are methods declared in an interface with a body, marked with the default keyword, providing a concrete implementation that implementing classes inherit automatically without being forced to override it. Before Java 8, interfaces could declare only abstract methods, meaning adding any new method to an interface would break every existing implementation — a problem that became critical when the Streams API needed to add methods like forEach(), stream(), and removeIf() to the existing Collection-family interfaces without breaking the entire ecosystem of third-party collection implementations. Default methods solved this by allowing interface evolution: new methods can be added to an interface with a default implementation, and existing implementing classes continue to compile and work without modification, inheriting the default behavior until they choose to override it. This entry covers the design motivation and interface evolution problem default methods solve, the complete syntax and inheritance rules, the diamond problem and Java's resolution rules for conflicting default methods from multiple interfaces, the explicit disambiguation syntax using InterfaceName.super.method(), the interaction between default methods and abstract classes, and the static method companion feature introduced alongside default methods.