Contents
- Core Functional Interface Types
- Function Composition
- Predicate Composition
- Method References
- Custom Functional Interfaces
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.