- Master Function, Predicate, Consumer, and Supplier
- Learn to combine and chain functional interfaces
- Understand binary variants and operators
- Use primitive specializations to avoid boxing
Built-in Functional Interfaces
You've been using functional interfaces—Comparator, Runnable, Callable. Java 8 added a comprehensive set in java.util.function. Understanding these saves you from creating custom interfaces.
The Core Four Functional Interfaces
Function<T, R>
Takes one argument of type T, returns result of type R.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Examples:
// String to Integer
Function<String, Integer> length = s -> s.length();
length.apply("Hello"); // 5
// Integer to String
Function<Integer, String> intToString = n -> "Number: " + n;
intToString.apply(42); // "Number: 42"
// Object transformation
Function<Person, String> getName = Person::getName;
Chaining Functions:
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
// Compose: first trim, then uppercase
Function<String, String> process = trim.andThen(upper);
process.apply(" hello "); // "HELLO"
// Or reverse order with compose
Function<String, String> process2 = upper.compose(trim);
Predicate
Takes one argument, returns boolean.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Examples:
Predicate<String> isEmpty = String::isEmpty;
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Person> isAdult = p -> p.getAge() >= 18;
isEmpty.test(""); // true
isPositive.test(-5); // false
Combining Predicates:
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;
// AND
Predicate<Integer> isPositiveAndEven = isPositive.and(isEven);
isPositiveAndEven.test(4); // true
isPositiveAndEven.test(-4); // false
// OR
Predicate<Integer> isPositiveOrEven = isPositive.or(isEven);
isPositiveOrEven.test(-4); // true (even)
// NOT
Predicate<Integer> isNotPositive = isPositive.negate();
isNotPositive.test(-5); // true
Consumer
Takes one argument, returns nothing (void).
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Examples:
Consumer<String> printer = System.out::println;
Consumer<List<String>> clearer = List::clear;
Consumer<Person> sendEmail = p -> emailService.send(p.getEmail());
printer.accept("Hello"); // prints "Hello"
Chaining Consumers:
Consumer<String> log = s -> System.out.println("[LOG] " + s);
Consumer<String> save = s -> database.save(s);
Consumer<String> logAndSave = log.andThen(save);
logAndSave.accept("Data"); // Logs then saves
Supplier
Takes no arguments, returns a value.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Examples:
Supplier<Double> random = Math::random;
Supplier<LocalDateTime> now = LocalDateTime::now;
Supplier<List<String>> listFactory = ArrayList::new;
random.get(); // 0.7234...
now.get(); // 2024-01-15T10:30:00
listFactory.get(); // new empty ArrayList
Use case - Lazy initialization:
public T getOrDefault(T value, Supplier<T> defaultSupplier) {
return value != null ? value : defaultSupplier.get();
}
// Expensive default only computed if needed
String result = getOrDefault(null, () -> computeExpensiveDefault());
Binary Variants
BiFunction<T, U, R>
Takes two arguments, returns a result.
BiFunction<String, String, Integer> compare = String::compareTo;
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = String::repeat;
compare.apply("a", "b"); // -1
add.apply(3, 4); // 7
repeat.apply("ab", 3); // "ababab"
BiPredicate<T, U>
Takes two arguments, returns boolean.
BiPredicate<String, String> startsWith = String::startsWith;
BiPredicate<Integer, Integer> greaterThan = (a, b) -> a > b;
startsWith.test("Hello", "He"); // true
greaterThan.test(5, 3); // true
BiConsumer<T, U>
Takes two arguments, returns nothing.
BiConsumer<String, Integer> printRepeat = (s, n) -> {
for (int i = 0; i < n; i++) System.out.println(s);
};
BiConsumer<Map<String, Integer>, String> incrementKey =
(map, key) -> map.merge(key, 1, Integer::sum);
printRepeat.accept("Hi", 3); // prints "Hi" 3 times
Operator Variants
UnaryOperator
Special case of Function where input and output are same type.
// UnaryOperator<T> extends Function<T, T>
UnaryOperator<Integer> square = n -> n * n;
UnaryOperator<String> addBang = s -> s + "!";
square.apply(5); // 25
addBang.apply("Hi"); // "Hi!"
// Used in replaceAll
List<String> names = new ArrayList<>(Arrays.asList("alice", "bob"));
names.replaceAll(String::toUpperCase); // UnaryOperator<String>
BinaryOperator
Special case of BiFunction where all types are same.
// BinaryOperator<T> extends BiFunction<T, T, T>
BinaryOperator<Integer> add = (a, b) -> a + b;
BinaryOperator<Integer> max = Math::max;
BinaryOperator<String> concat = String::concat;
add.apply(3, 4); // 7
max.apply(10, 5); // 10
concat.apply("a", "b"); // "ab"
// Used in reduce
int sum = numbers.stream().reduce(0, Integer::sum); // BinaryOperator<Integer>
Primitive Specializations
To avoid boxing overhead, Java provides primitive variants:
For int
IntFunction<String> intToString = n -> "Value: " + n;
IntPredicate isEven = n -> n % 2 == 0;
IntConsumer printInt = System.out::println;
IntSupplier randomInt = () -> (int) (Math.random() * 100);
IntUnaryOperator doubleIt = n -> n * 2;
IntBinaryOperator sum = (a, b) -> a + b;
// Conversions
ToIntFunction<String> length = String::length;
IntToDoubleFunction toDouble = n -> n * 1.0;
For long and double
LongPredicate isPositive = n -> n > 0;
DoubleFunction<String> format = d -> String.format("%.2f", d);
ToLongFunction<String> parseLong = Long::parseLong;
ToDoubleFunction<Integer> toPercent = n -> n / 100.0;
Summary Table
| Interface | Parameters | Returns | Method |
|---|---|---|---|
Function<T,R> |
T | R | apply(T) |
BiFunction<T,U,R> |
T, U | R | apply(T,U) |
Predicate<T> |
T | boolean | test(T) |
BiPredicate<T,U> |
T, U | boolean | test(T,U) |
Consumer<T> |
T | void | accept(T) |
BiConsumer<T,U> |
T, U | void | accept(T,U) |
Supplier<T> |
none | T | get() |
UnaryOperator<T> |
T | T | apply(T) |
BinaryOperator<T> |
T, T | T | apply(T,T) |
Choosing the Right Interface
| Need | Use |
|---|---|
| Transform a value | Function<T,R> |
| Test a condition | Predicate<T> |
| Consume a value (side effect) | Consumer<T> |
| Produce a value | Supplier<T> |
| Transform same type | UnaryOperator<T> |
| Combine two same-type values | BinaryOperator<T> |
| Two inputs, one output | BiFunction<T,U,R> |
| Avoid primitive boxing | IntXxx, LongXxx, DoubleXxx |
Java's built-in functional interfaces cover most cases. Function transforms, Predicate tests conditions, Consumer performs side effects, Supplier creates values lazily. Combine predicates with and(), or(), negate(). Chain functions with andThen(), compose(). Use primitive specializations to avoid boxing. Prefer built-in interfaces over creating your own.
