For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/java/functional-interfaces.md.

Java functional interfaces guide: Function, Predicate, Consumer, Supplier, method references, composition, and writing custom functional interfaces.

Functional Interfaces and Lambdas

A functional interface is an interface with exactly one abstract method. Lambdas and method references are shorthand for implementing these interfaces. Together, they are the foundation of Java's functional programming style - streams, Optional, and CompletableFuture all depend on them.

Quick start

// Lambda implementing Predicate<String>
Predicate<String> isLong = s -> s.length() > 5;

// Method reference implementing Function<String, Integer>
Function<String, Integer> toLength = String::length;

// Using them
List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");

List<Integer> longNameLengths = names.stream()
    .filter(isLong)               // keep names with length > 5
    .map(toLength)
    .toList();
// Result: [7] (only "Charlie" - "Diana" is length 5, which is not > 5)

Built-in functional interfaces

Java provides a rich set in java.util.function:

Core four

InterfaceSignaturePurposeExample
Function<T, R>R apply(T t)Transform T to RString::length
Predicate<T>boolean test(T t)Test a conditions -> s.isEmpty()
Consumer<T>void accept(T t)Perform a side effectSystem.out::println
Supplier<T>T get()Produce a value() -> new ArrayList<>()

Two-argument variants

InterfaceSignatureExample
BiFunction<T, U, R>R apply(T t, U u)(a, b) -> a + b
BiPredicate<T, U>boolean test(T t, U u)(s, len) -> s.length() > len
BiConsumer<T, U>void accept(T t, U u)map::put

Specialised variants

InterfaceSignatureAvoids
UnaryOperator<T>T apply(T t)Function<T, T>
BinaryOperator<T>T apply(T t1, T t2)BiFunction<T, T, T>
IntFunction<R>R apply(int value)Boxing
ToIntFunction<T>int applyAsInt(T value)Boxing
IntPredicateboolean test(int value)Boxing
IntConsumervoid accept(int value)Boxing
IntSupplierint getAsInt()Boxing

Lambda syntax

// Full form
(String s) -> { return s.length(); }

// Inferred parameter type
(s) -> { return s.length(); }

// Single parameter - parentheses optional
s -> { return s.length(); }

// Single expression - braces and return optional
s -> s.length()

// No parameters
() -> System.out.println("hello")

// Multiple parameters
(a, b) -> a + b

// Multiple statements (need braces and return)
(a, b) -> {
    int sum = a + b;
    System.out.println("Sum: " + sum);
    return sum;
}

Method references

Method references are a shorthand for lambdas that just call an existing method:

TypeSyntaxEquivalent lambda
Static methodInteger::parseInts -> Integer.parseInt(s)
Instance method (on parameter)String::toUpperCases -> s.toUpperCase()
Instance method (on object)System.out::printlns -> System.out.println(s)
ConstructorArrayList::new() -> new ArrayList<>()
List<String> words = List.of("hello", "world");

// Static method reference
List<Integer> parsed = List.of("1", "2", "3").stream()
    .map(Integer::parseInt)
    .toList();

// Instance method reference (on each element)
List<String> upper = words.stream()
    .map(String::toUpperCase)
    .toList();

// Bound instance method reference (on a specific object)
words.stream().forEach(System.out::println);

// Constructor reference
List<StringBuilder> builders = words.stream()
    .map(StringBuilder::new)          // equivalent to: w -> new StringBuilder(w)
    .toList();

Composing functions

Functional interfaces provide default methods for composition:

Function composition

Function<String, String> trim = String::strip;
Function<String, String> lower = String::toLowerCase;
Function<String, Integer> length = String::length;

// andThen: apply trim, then lower, then length
Function<String, Integer> pipeline = trim.andThen(lower).andThen(length);
pipeline.apply("  Hello World  "); // 11

// compose: apply lower first, then trim (reverse order)
Function<String, String> composed = trim.compose(lower);
composed.apply("  HELLO  "); // "hello"

Predicate composition

Predicate<String> isNotEmpty = s -> !s.isEmpty();
Predicate<String> isShort = s -> s.length() < 5;
Predicate<String> startsWithA = s -> s.startsWith("A");

// Combine with and, or, negate
Predicate<String> shortNonEmptyStartingWithA =
    isNotEmpty.and(isShort).and(startsWithA);

Predicate<String> longOrStartsWithA =
    isShort.negate().or(startsWithA);

List<String> names = List.of("Alice", "Bob", "Al", "", "Charlie", "Amy");

List<String> result = names.stream()
    .filter(shortNonEmptyStartingWithA)
    .toList();
// [Al, Amy]

Consumer chaining

Consumer<String> log = s -> System.out.println("LOG: " + s);
Consumer<String> save = s -> database.save(s);

// andThen: log first, then save
Consumer<String> logAndSave = log.andThen(save);
logAndSave.accept("event happened");

Writing custom functional interfaces

Use @FunctionalInterface to enforce exactly one abstract method:

@FunctionalInterface
interface Validator<T> {
    boolean isValid(T value);

    // Default methods are allowed
    default Validator<T> and(Validator<T> other) {
        return value -> this.isValid(value) && other.isValid(value);
    }

    default Validator<T> or(Validator<T> other) {
        return value -> this.isValid(value) || other.isValid(value);
    }

    default Validator<T> negate() {
        return value -> !this.isValid(value);
    }
}

// Usage
Validator<String> notBlank = s -> s != null && !s.isBlank();
Validator<String> maxLength = s -> s.length() <= 100;
Validator<String> noHtml = s -> !s.contains("<");

Validator<String> safeInput = notBlank.and(maxLength).and(noHtml);

safeInput.isValid("Hello");          // true
safeInput.isValid("");               // false
safeInput.isValid("<script>alert"); // false

Functional interface with generics

@FunctionalInterface
interface Transformer<T, R> {
    R transform(T input);

    default <V> Transformer<T, V> andThen(Transformer<R, V> after) {
        return input -> after.transform(this.transform(input));
    }
}

Transformer<String, Integer> toLength = String::length;
Transformer<Integer, Boolean> isEven = n -> n % 2 == 0;

Transformer<String, Boolean> hasEvenLength = toLength.andThen(isEven);
hasEvenLength.transform("hello"); // false (5 is odd)
hasEvenLength.transform("Java");  // true (4 is even)

Practical examples

Configurable retry logic

public static <T> T retry(Supplier<T> action, int maxAttempts, Predicate<Exception> retryable) {
    for (int attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            return action.get();
        } catch (Exception e) {
            if (attempt == maxAttempts || !retryable.test(e)) {
                throw e;
            }
        }
    }
    throw new IllegalStateException("unreachable");
}

// Usage
String result = retry(
    () -> httpClient.send(request),      // Supplier<String>
    3,                                   // max attempts
    e -> e instanceof IOException        // Predicate<Exception>
);

Strategy pattern with lambdas

record Order(double total) {}

// Instead of a Strategy interface with implementations:
Function<Order, Double> flatDiscount = order -> order.total() - 10;
Function<Order, Double> percentDiscount = order -> order.total() * 0.9;
Function<Order, Double> noDiscount = Order::total;

// Select strategy at runtime
Function<Order, Double> strategy = isPremium ? percentDiscount : flatDiscount;
double finalPrice = strategy.apply(new Order(100.0));

Common pitfalls

PitfallProblemFix
Checked exceptions in lambdasFunction<T,R> does not allow checked exceptionsWrap in try/catch inside the lambda, or create a custom functional interface that throws
Capturing mutable variablesfor (int i...) - lambda captures effectively final variables onlyUse a final copy or IntStream
this in lambdasthis refers to the enclosing class, not the lambdaExpected behavior, but can be surprising
Overusing lambdasComplex multi-line lambdas are hard to readExtract to a named method and use a method reference
Missing @FunctionalInterfaceNon-annotated interfaces can accidentally gain a second abstract methodAlways annotate custom functional interfaces