Custom Functional Interfaces

@FunctionalInterface

You can write your own functional interface. Mark it with @FunctionalInterface: the compiler then fails if someone adds a second abstract method. default and static methods are allowed, because they are not abstract.

Example

A custom interface for discount rules, with a default method to chain two rules.

public class Main {
    public static void main(String[] args) {
        DiscountRule festive = amount -> amount * 0.10;
        DiscountRule flat100 = amount -> amount >= 500 ? 100 : 0;
        DiscountRule both = festive.plus(flat100);

        double bill = 2000;
        System.out.println("Festive: " + festive.discount(bill));
        System.out.println("Flat:    " + flat100.discount(bill));
        System.out.println("Both:    " + both.discount(bill));
        System.out.println("Payable: " + (bill - both.discount(bill)));
    }
}

@FunctionalInterface
interface DiscountRule {
    double discount(double amount);

    default DiscountRule plus(DiscountRule other) {
        return amount -> this.discount(amount) + other.discount(amount);
    }
}

Output

Festive: 200.0
Flat:    100.0
Both:    300.0
Payable: 1700.0

Checked Exceptions in Lambdas

The built-in interfaces do not declare checked exceptions, so a lambda that calls Files.readString must catch IOException inside. A common fix is your own interface that throws Exception.

@FunctionalInterface
interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;

    static <T, R> Function<T, R> unchecked(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.unchecked(Files::readString))
        .toList();