What makes an interface work with lambdas? It must have exactly one abstract method - a functional interface. Java provides Function, Predicate, Consumer, and more. You can create your own too.

Define a functional interface

Interface with exactly one abstract method.

Definition.java
public class Definition {

    @FunctionalInterface
    interface Formatter {
        String format(String input);

        default String formatOrEmpty(String input) {
            if (input == null) return "";
            return format(input);
        }

        static Formatter identity() {
            return s -> s;
        }
    }

    public static void main(String[] args) {
        Formatter upper = s -> s.toUpperCase();
        String sample = ;
        System.out.println("upper: " + upper.format(sample));

        System.out.println("default: '" + upper.formatOrEmpty(null) + "'");

        Formatter id = Formatter.identity();
        System.out.println("identity: " + id.format("same"));

    }
}
public class Definition {

    @FunctionalInterface
    interface Formatter {
        String format(String input);

        default String formatOrEmpty(String input) {
            if (input == null) return "";
            return format(input);
        }

        static Formatter identity() {
            return s -> s;
        }
    }

    public static void main(String[] args) {
        Formatter upper = s -> s.toUpperCase();
        String sample = ;
        System.out.println("upper: " + upper.format(sample));

        System.out.println("default: '" + upper.formatOrEmpty(null) + "'");

        Formatter id = Formatter.identity();
        System.out.println("identity: " + id.format("same"));

    }
}
public class Definition {

    @FunctionalInterface
    interface Formatter {
        String format(String input);

        default String formatOrEmpty(String input) {
            if (input == null) return "";
            return format(input);
        }

        static Formatter identity() {
            return s -> s;
        }
    }

    public static void main(String[] args) {
        Formatter upper = s -> s.toUpperCase();
        String sample = ;
        System.out.println("upper: " + upper.format(sample));

        System.out.println("default: '" + upper.formatOrEmpty(null) + "'");

        Formatter id = Formatter.identity();
        System.out.println("identity: " + id.format("same"));

    }
}

@FunctionalInterface annotation ensures single abstract method.

functional interface Interface with one abstract method. Target type for lambda expressions.

Built-in functional interfaces

Java's standard functional interfaces.

Builtin.java
import java.util.function.*;

public class Builtin {
    public static void main(String[] args) {
        Predicate<String> isLong = s -> s.length() >= 5;
        System.out.println("isLong('cat') = " + isLong.test("cat"));
        System.out.println("isLong('tiger') = " + isLong.test("tiger"));

        Function<String, Integer> length = s -> s.length();
        System.out.println("length('hello') = " + length.apply("hello"));

        Consumer<String> printer = s -> System.out.println("print: " + s);
        printer.accept("message");

        long fixedMillis = 1700000000000L;
        Supplier<Long> nowMillis = () -> fixedMillis;
        System.out.println("nowMillis() = " + nowMillis.get());

        UnaryOperator<String> trim = s -> s.trim();
        System.out.println("trim('  hi  ') = '" + trim.apply("  hi  ") + "'");

        BiFunction<Integer, Integer, Integer> max = (a, b) -> a > b ? a : b;
        System.out.println("max(3, 7) = " + max.apply(3, 7));

    }
}

Function<T,R>, Predicate<T>, Consumer<T>, Supplier<T> - learn these.

Function `Function<T,R>`: takes T, returns R. Method: `R apply(T t)`.
Predicate `Predicate<T>`: takes T, returns boolean. Method: `boolean test(T t)`.

Function composition

Chain functions together.

Composition.java
import java.util.function.*;

public class Composition {
    public static void main(String[] args) {
        Function<String, String> trim = s -> s.trim();
        Function<String, String> upper = s -> s.toUpperCase();
        Function<String, Integer> length = s -> s.length();

        String text = ;
        Function<String, String> upperAfterTrim = upper.compose(trim);
        System.out.println("upperAfterTrim('" + text + "') = " + upperAfterTrim.apply(text));

        Function<String, Integer> lenAfterUpperTrim = upperAfterTrim.andThen(length);
        System.out.println("lenAfterUpperTrim('" + text + "') = " + lenAfterUpperTrim.apply(text));

        Predicate<String> nonEmpty = s -> !s.isEmpty();
        Predicate<String> startsWithA = s -> s.startsWith("A");

        Predicate<String> ok = nonEmpty.and(startsWithA);
        System.out.println("ok('') = " + ok.test(""));
        System.out.println("ok('Bob') = " + ok.test("Bob"));
        System.out.println("ok('Alice') = " + ok.test("Alice"));

        System.out.println("not startsWithA('Bob') = " + startsWithA.negate().test("Bob"));

        Consumer<String> a = s -> System.out.print("[" + s + "]");
        Consumer<String> b = s -> System.out.println(" (len=" + s.length() + ")");
        Consumer<String> both = a.andThen(b);
        both.accept("hello");

    }
}
import java.util.function.*;

public class Composition {
    public static void main(String[] args) {
        Function<String, String> trim = s -> s.trim();
        Function<String, String> upper = s -> s.toUpperCase();
        Function<String, Integer> length = s -> s.length();

        String text = ;
        Function<String, String> upperAfterTrim = upper.compose(trim);
        System.out.println("upperAfterTrim('" + text + "') = " + upperAfterTrim.apply(text));

        Function<String, Integer> lenAfterUpperTrim = upperAfterTrim.andThen(length);
        System.out.println("lenAfterUpperTrim('" + text + "') = " + lenAfterUpperTrim.apply(text));

        Predicate<String> nonEmpty = s -> !s.isEmpty();
        Predicate<String> startsWithA = s -> s.startsWith("A");

        Predicate<String> ok = nonEmpty.and(startsWithA);
        System.out.println("ok('') = " + ok.test(""));
        System.out.println("ok('Bob') = " + ok.test("Bob"));
        System.out.println("ok('Alice') = " + ok.test("Alice"));

        System.out.println("not startsWithA('Bob') = " + startsWithA.negate().test("Bob"));

        Consumer<String> a = s -> System.out.print("[" + s + "]");
        Consumer<String> b = s -> System.out.println(" (len=" + s.length() + ")");
        Consumer<String> both = a.andThen(b);
        both.accept("hello");

    }
}
import java.util.function.*;

public class Composition {
    public static void main(String[] args) {
        Function<String, String> trim = s -> s.trim();
        Function<String, String> upper = s -> s.toUpperCase();
        Function<String, Integer> length = s -> s.length();

        String text = ;
        Function<String, String> upperAfterTrim = upper.compose(trim);
        System.out.println("upperAfterTrim('" + text + "') = " + upperAfterTrim.apply(text));

        Function<String, Integer> lenAfterUpperTrim = upperAfterTrim.andThen(length);
        System.out.println("lenAfterUpperTrim('" + text + "') = " + lenAfterUpperTrim.apply(text));

        Predicate<String> nonEmpty = s -> !s.isEmpty();
        Predicate<String> startsWithA = s -> s.startsWith("A");

        Predicate<String> ok = nonEmpty.and(startsWithA);
        System.out.println("ok('') = " + ok.test(""));
        System.out.println("ok('Bob') = " + ok.test("Bob"));
        System.out.println("ok('Alice') = " + ok.test("Alice"));

        System.out.println("not startsWithA('Bob') = " + startsWithA.negate().test("Bob"));

        Consumer<String> a = s -> System.out.print("[" + s + "]");
        Consumer<String> b = s -> System.out.println(" (len=" + s.length() + ")");
        Consumer<String> both = a.andThen(b);
        both.accept("hello");

    }
}

f.andThen(g) - apply f, then g. f.compose(g) - apply g, then f.

Custom generic functional interface

Create your own functional interfaces.

CustomGeneric.java
import java.util.*;

public class CustomGeneric {

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

        default Validator<T> and(Validator<T> other) {
            return v -> this.isValid(v) && other.isValid(v);
        }

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

    private static <T> List<T> filter(List<T> items, Validator<T> validator) {
        List<T> out = new ArrayList<>();
        for (T item : items) {
            if (validator.isValid(item)) {
                out.add(item);
            }
        }
        return out;
    }

    public static void main(String[] args) {
        Validator<String> nonEmpty = s -> s != null && !s.isEmpty();
        Validator<String> hasAt = s -> s.contains("@");

        Validator<String> emailLike = nonEmpty.and(hasAt);

        List<String> inputs = Arrays.asList("", "alice", "alice@example.com", "@", "bob@x");
        System.out.println("inputs: " + inputs);
        System.out.println("emailLike: " + filter(inputs, emailLike));

        Validator<String> empty = s -> s.isEmpty();
        Validator<String> emptyOrEmail = empty.or(emailLike);
        System.out.println("emptyOrEmail: " + filter(inputs, emptyOrEmail));

    }
}

Define with generics for maximum reusability.

Pass behavior as parameter

Methods that accept functional interfaces.

PassBehavior.java
import java.util.*;
import java.util.function.*;

public class PassBehavior {

    private static <T, R> List<R> map(List<T> items, Function<T, R> fn) {
        List<R> out = new ArrayList<>();
        for (T item : items) {
            out.add(fn.apply(item));
        }
        return out;
    }

    private static <T> List<T> filter(List<T> items, Predicate<T> pred) {
        List<T> out = new ArrayList<>();
        for (T item : items) {
            if (pred.test(item)) {
                out.add(item);
            }
        }
        return out;
    }

    private static int reduceInts(List<Integer> items, int identity, IntBinaryOperator op) {
        int acc = identity;
        for (int v : items) {
            acc = op.applyAsInt(acc, v);
        }
        return acc;
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        System.out.println("lengths: " + map(names, s -> s.length()));

        int minLength = ;
        System.out.println("long names: " + filter(names, s -> s.length() >= minLength));

        List<Integer> nums = Arrays.asList(1, 2, 3, 4);
        int sum = reduceInts(nums, 0, (a, b) -> a + b);
        int product = reduceInts(nums, 1, (a, b) -> a * b);
        System.out.println("sum = " + sum);
        System.out.println("product = " + product);

    }
}
import java.util.*;
import java.util.function.*;

public class PassBehavior {

    private static <T, R> List<R> map(List<T> items, Function<T, R> fn) {
        List<R> out = new ArrayList<>();
        for (T item : items) {
            out.add(fn.apply(item));
        }
        return out;
    }

    private static <T> List<T> filter(List<T> items, Predicate<T> pred) {
        List<T> out = new ArrayList<>();
        for (T item : items) {
            if (pred.test(item)) {
                out.add(item);
            }
        }
        return out;
    }

    private static int reduceInts(List<Integer> items, int identity, IntBinaryOperator op) {
        int acc = identity;
        for (int v : items) {
            acc = op.applyAsInt(acc, v);
        }
        return acc;
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        System.out.println("lengths: " + map(names, s -> s.length()));

        int minLength = ;
        System.out.println("long names: " + filter(names, s -> s.length() >= minLength));

        List<Integer> nums = Arrays.asList(1, 2, 3, 4);
        int sum = reduceInts(nums, 0, (a, b) -> a + b);
        int product = reduceInts(nums, 1, (a, b) -> a * b);
        System.out.println("sum = " + sum);
        System.out.println("product = " + product);

    }
}
import java.util.*;
import java.util.function.*;

public class PassBehavior {

    private static <T, R> List<R> map(List<T> items, Function<T, R> fn) {
        List<R> out = new ArrayList<>();
        for (T item : items) {
            out.add(fn.apply(item));
        }
        return out;
    }

    private static <T> List<T> filter(List<T> items, Predicate<T> pred) {
        List<T> out = new ArrayList<>();
        for (T item : items) {
            if (pred.test(item)) {
                out.add(item);
            }
        }
        return out;
    }

    private static int reduceInts(List<Integer> items, int identity, IntBinaryOperator op) {
        int acc = identity;
        for (int v : items) {
            acc = op.applyAsInt(acc, v);
        }
        return acc;
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        System.out.println("lengths: " + map(names, s -> s.length()));

        int minLength = ;
        System.out.println("long names: " + filter(names, s -> s.length() >= minLength));

        List<Integer> nums = Arrays.asList(1, 2, 3, 4);
        int sum = reduceInts(nums, 0, (a, b) -> a + b);
        int product = reduceInts(nums, 1, (a, b) -> a * b);
        System.out.println("sum = " + sum);
        System.out.println("product = " + product);

    }
}

Accept Predicate<T> to let caller define filtering logic.

Exercise: Practical.java

Build a data processor using functional interfaces