Contents

The java.util.function package organizes its interfaces around four fundamental shapes. Function<T, R> transforms an input of type T into an output of type R — it is the general transformation type. Predicate<T> tests a condition and returns a primitive boolean, used everywhere filtering is needed. Consumer<T> accepts a value and produces no result — it models side-effectful operations like printing or writing. Supplier<T> takes no arguments and produces a value, used for lazy evaluation and factory patterns. The Bi- variants (BiFunction, BiPredicate, BiConsumer) handle two-argument cases. UnaryOperator<T> and BinaryOperator<T> are specializations of Function where input and output types are the same. Primitive specializations like IntFunction, ToIntFunction, and IntUnaryOperator avoid autoboxing overhead in performance-sensitive code.

import java.util.function.*; // Function<T, R> — takes T, returns R Function<String, Integer> length = String::length; Function<String, String> upper = String::toUpperCase; System.out.println(length.apply("hello")); // 5 System.out.println(upper.apply("hello")); // HELLO // BiFunction<T, U, R> — takes two args, returns R BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n); System.out.println(repeat.apply("ab", 3)); // ababab // Predicate<T> — takes T, returns boolean Predicate<String> isBlank = String::isBlank; Predicate<Integer> isEven = n -> n % 2 == 0; System.out.println(isBlank.test(" ")); // true System.out.println(isEven.test(4)); // true // Supplier<T> — takes nothing, returns T (lazy producer) Supplier<List<String>> listFactory = ArrayList::new; List<String> list = listFactory.get(); // Consumer<T> — takes T, returns void Consumer<String> print = System.out::println; print.accept("Hello"); // prints Hello // BiConsumer<T, U> BiConsumer<String, Integer> printRepeat = (s, n) -> System.out.println(s.repeat(n)); // UnaryOperator<T> — special Function where T and R are the same type UnaryOperator<String> trim = String::trim; // BinaryOperator<T> — BiFunction where all types are the same BinaryOperator<Integer> add = Integer::sum; System.out.println(add.apply(3, 4)); // 7 // Primitive specializations — avoid boxing overhead IntFunction<String> intToStr = Integer::toString; ToIntFunction<String> strToLen = String::length; IntUnaryOperator doubleIt = x -> x * 2; IntBinaryOperator multiply = (a, b) -> a * b; IntSupplier randomInt = () -> 42; IntConsumer printInt = System.out::println; IntPredicate positive = x -> x > 0;

Function provides two composition methods that let you build transformation pipelines from small, reusable pieces. f.andThen(g) produces a new function that applies f first and then feeds the result to g — reading left to right mirrors execution order, making pipelines easy to reason about. f.compose(g) reverses this: it applies g first and then f, so f.compose(g) is mathematically equivalent to the function composition f(g(x)). Function.identity() returns the input unchanged and serves as the identity element for reduce-based dynamic pipeline construction. These composition methods return a new Function without modifying the originals, so individual steps remain reusable and testable in isolation.

Function<String, String> trim = String::trim; Function<String, String> upper = String::toUpperCase; Function<String, Integer> length = String::length; // andThen — apply this, then apply after // trim.andThen(upper) is equivalent to: s -> upper.apply(trim.apply(s)) Function<String, String> trimThenUpper = trim.andThen(upper); System.out.println(trimThenUpper.apply(" hello ")); // HELLO // compose — apply before first, then apply this // upper.compose(trim) is equivalent to: s -> upper.apply(trim.apply(s)) Function<String, String> sameResult = upper.compose(trim); System.out.println(sameResult.apply(" hello ")); // HELLO // Chain multiple steps Function<String, Integer> pipeline = trim .andThen(upper) .andThen(length); System.out.println(pipeline.apply(" hello ")); // 5 // Function.identity() — returns the input unchanged Function<String, String> identity = Function.identity(); // Compose a list of functions dynamically List<Function<String, String>> transforms = List.of( String::trim, String::toUpperCase, s -> s + "!"); Function<String, String> combined = transforms.stream() .reduce(Function.identity(), Function::andThen); System.out.println(combined.apply(" hello ")); // HELLO! // UnaryOperator composition UnaryOperator<Integer> double_ = x -> x * 2; UnaryOperator<Integer> addTen = x -> x + 10; Function<Integer, Integer> doubleThenAdd = double_.andThen(addTen); System.out.println(doubleThenAdd.apply(5)); // 20

Predicate has three logical composition methods: and(), or(), and negate(). Both and() and or() are short-circuit: and() skips the second predicate when the first returns false, and or() skips it when the first returns true. The practical benefit is that you can define small, single-purpose predicates — notBlank, isShort, hasDigit — and combine them at the call site into precise filter expressions without writing a new lambda for every combination. Predicate.not() (Java 11+) is a static factory that negates a method reference cleanly, avoiding the awkward cast required when calling .negate() on a method reference directly.

Predicate<String> notBlank = Predicate.not(String::isBlank); Predicate<String> shortStr = s -> s.length() < 10; Predicate<String> hasDigit = s -> s.chars().anyMatch(Character::isDigit); // and — logical AND (short-circuits) Predicate<String> validInput = notBlank.and(shortStr).and(hasDigit); System.out.println(validInput.test("abc123")); // true System.out.println(validInput.test("abc")); // false (no digit) // or — logical OR (short-circuits) Predicate<String> lenient = shortStr.or(hasDigit); System.out.println(lenient.test("verylongstring123")); // true (has digit) // negate — logical NOT Predicate<String> longOrBlank = shortStr.negate().or(String::isBlank); // Predicate.not() — static convenience (Java 11+) List<String> words = List.of("", "hello", " ", "world"); words.stream() .filter(Predicate.not(String::isBlank)) .forEach(System.out::println); // hello, world // BiPredicate<T, U> BiPredicate<String, String> startsWith = (s, prefix) -> s.startsWith(prefix); System.out.println(startsWith.test("hello", "he")); // true // Combining predicates in a filtering pipeline Predicate<Integer> between10and100 = ((Predicate<Integer>)(n -> n > 10)) .and(n -> n < 100); List.of(5, 15, 50, 150).stream() .filter(between10and100) .forEach(System.out::println); // 15, 50

Method references are syntactic sugar for lambdas that do nothing except call a single existing method. There are four kinds. A static method reference (Class::staticMethod) maps directly to a static call. An instance method reference on a specific object (obj::method) captures that particular instance and calls its method. An instance method reference on an arbitrary object of a type (Class::instanceMethod) receives the instance as the lambda's first argument — the compiler infers that the first parameter is the receiver, so String::toUpperCase satisfies Function<String, String>. A constructor reference (Class::new) calls a constructor and can satisfy a Supplier, Function, or BiFunction depending on arity. All four are verified by the compiler for type safety at the point of assignment.

// Four kinds of method references: // 1. Static method reference: ClassName::staticMethod Function<String, Integer> parse = Integer::parseInt; // Integer.parseInt(s) Function<Double, Double> abs = Math::abs; // 2. Instance method of a particular object: instance::method String prefix = "Hello, "; Function<String, String> greet = prefix::concat; System.out.println(greet.apply("Alice")); // Hello, Alice // 3. Instance method of an arbitrary object of a type: ClassName::instanceMethod // (receiver becomes the first argument) Function<String, String> toUpper = String::toUpperCase; // s.toUpperCase() Function<String, Integer> getLen = String::length; // s.length() BiFunction<String, String, Boolean> startsWith = String::startsWith; // 4. Constructor reference: ClassName::new Supplier<ArrayList<String>> listMaker = ArrayList::new; Function<Integer, ArrayList<String>> sizedList = ArrayList::new; BiFunction<String, Integer, StringBuilder> sbFactory = (s, cap) -> new StringBuilder(s); // no matching constructor, use lambda // Method references with streams List<String> names = List.of("alice", "bob", "charlie"); names.stream() .map(String::toUpperCase) // instance method ref .sorted(String::compareTo) // instance method ref as Comparator .forEach(System.out::println); // static-like (PrintStream instance ref) // Comparator method references List<Person> people = getPeople(); people.sort(Comparator.comparing(Person::name) // getter ref .thenComparingInt(Person::age)); // int-returning getter

Any interface with exactly one abstract method is a functional interface and can be used with lambdas, but annotating it with @FunctionalInterface adds a compile-time check: the compiler will reject the interface if it has zero or more than one abstract method, preventing accidental breakage when the interface evolves. The two main reasons to define a custom functional interface rather than reusing a standard one are checked exceptions and semantic naming. The standard types do not declare checked exceptions, so lambdas that throw them require a try/catch inside; a custom ThrowingFunction<T, R> propagates the checked exception cleanly. Semantic naming is equally valuable: PasswordValidator communicates intent more precisely than Predicate<String> even though they have the same shape, making code at the call site self-documenting.

// @FunctionalInterface — annotate custom functional interfaces // The annotation enforces exactly one abstract method at compile time @FunctionalInterface interface ThrowingFunction<T, R> { R apply(T t) throws Exception; // Can have default methods — they don't count as the abstract method default <V> ThrowingFunction<T, V> andThen(ThrowingFunction<R, V> after) { return t -> after.apply(this.apply(t)); } // Static factory to wrap and re-throw checked exceptions static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> f) { return t -> { try { return f.apply(t); } catch (Exception e) { throw new RuntimeException(e); } }; } } // Usage: stream operations that throw checked exceptions List<String> paths = List.of("/etc/hosts", "/etc/resolv.conf"); paths.stream() .map(ThrowingFunction.wrap(path -> Files.readString(Path.of(path)))) .forEach(System.out::println); // Tri-function example @FunctionalInterface interface TriFunction<A, B, C, R> { R apply(A a, B b, C c); } TriFunction<Integer, Integer, Integer, Integer> clamp = (value, min, max) -> Math.min(Math.max(value, min), max); System.out.println(clamp.apply(15, 0, 10)); // 10 // Runnable and Callable — older functional interfaces that predate Java 8 // but are still used extensively Runnable r = () -> System.out.println("run"); Callable<Integer> c = () -> 42; // like Supplier but throws checked exceptions Custom functional interfaces are most useful when you need checked exception support or a more specific name that documents intent (e.g., Transformer<T> vs Function<T, T>). For everything else, prefer the standard java.util.function types to maximize interoperability with the JDK and third-party libraries.