Java 8 – Functional Interfaces: Predicate, Function, Consumer, Supplier
A functional interface is any interface with exactly one abstract method. Java 8 introduced the java.util.function package with four core functional interfaces that cover the most common patterns. Understanding them is essential for working with lambdas, streams, and method references.
The Four Core Functional Interfaces
| Interface | Method | Input → Output | Purpose |
|---|---|---|---|
Predicate<T> |
test(T t) |
T → boolean | Test a condition — returns true/false |
Function<T, R> |
apply(T t) |
T → R | Transform one type to another |
Consumer<T> |
accept(T t) |
T → void | Process a value, produce no result |
Supplier<T> |
get() |
() → T | Supply a value, take no input |
1. Predicate<T> – Test a Condition
import java.util.function.Predicate;
import java.util.*;
import java.util.stream.Collectors;
public class PredicateExample {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isLong = s -> s.length() > 5;
Predicate<Integer> isPositive = n -> n > 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false
System.out.println(isLong.test("Lambda")); // false (6 chars = not > 5... wait: 6 > 5 = true)
// Combining predicates
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
Predicate<Integer> isEvenOrNegative = isEven.or(isPositive.negate());
Predicate<Integer> isOdd = isEven.negate();
System.out.println(isEvenAndPositive.test(4)); // true
System.out.println(isEvenAndPositive.test(-2)); // false
System.out.println(isOdd.test(3)); // true
// Using Predicate with Stream
List<Integer> numbers = Arrays.asList(1, -2, 3, -4, 5, 6, -7, 8);
List<Integer> positiveEvens = numbers.stream()
.filter(isEvenAndPositive)
.collect(Collectors.toList());
System.out.println("Positive evens: " + positiveEvens); // [6, 8]
// Using Predicate with custom objects
List<String> names = Arrays.asList("Alice", "Bob", "Charlotte", "Dan", "Elizabeth");
Predicate<String> longName = s -> s.length() > 4;
names.stream()
.filter(longName)
.forEach(System.out::println);
// Alice, Charlotte, Elizabeth
}
}
2. Function<T, R> – Transform a Value
import java.util.function.Function;
import java.util.*;
import java.util.stream.Collectors;
public class FunctionExample {
public static void main(String[] args) {
Function<String, Integer> length = String::length;
Function<String, String> upper = String::toUpperCase;
Function<Integer, Integer> square = n -> n * n;
Function<Integer, String> intToStr = n -> "Number: " + n;
System.out.println(length.apply("Lambda")); // 6
System.out.println(upper.apply("java")); // JAVA
System.out.println(square.apply(5)); // 25
System.out.println(intToStr.apply(42)); // Number: 42
// Chaining with andThen() — apply f1, then f2 on the result
Function<String, String> upperAndTrim = upper.andThen(String::trim);
System.out.println(upperAndTrim.apply(" hello ")); // HELLO (trimmed after uppercase)
// compose() — apply f2 first, then f1
Function<Integer, Integer> doubleSquare = square.compose(n -> n * 2);
// compose: first n*2, then square → (5*2)^2 = 100
System.out.println(doubleSquare.apply(5)); // 100
// Using Function with Stream
List<String> words = Arrays.asList("apple", "banana", "cherry");
List<Integer> lengths = words.stream()
.map(length)
.collect(Collectors.toList());
System.out.println("Lengths: " + lengths); // [5, 6, 6]
// BiFunction — two inputs
java.util.function.BiFunction<String, Integer, String> repeat =
(s, n) -> s.repeat(n);
System.out.println(repeat.apply("Java", 3)); // JavaJavaJava
}
}
3. Consumer<T> – Consume a Value
import java.util.function.Consumer;
import java.util.*;
public class ConsumerExample {
public static void main(String[] args) {
Consumer<String> print = System.out::println;
Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
Consumer<Integer> printSquare = n -> System.out.println(n + "^2 = " + (n*n));
print.accept("Hello Consumer!"); // Hello Consumer!
printUpper.accept("java 8"); // JAVA 8
printSquare.accept(5); // 5^2 = 25
// andThen() — chain two consumers, both run on the same input
Consumer<String> printBoth = print.andThen(printUpper);
printBoth.accept("lambda");
// lambda
// LAMBDA
// Using Consumer with forEach
List<String> products = Arrays.asList("Laptop", "Mouse", "Keyboard");
products.forEach(print);
// BiConsumer — two inputs
java.util.function.BiConsumer<String, Double> printProduct =
(name, price) -> System.out.printf("%-10s ₹%.2f%n", name, price);
printProduct.accept("Laptop", 75000.0); // Laptop ₹75000.00
printProduct.accept("Mouse", 1500.0); // Mouse ₹1500.00
Map<String, Double> catalog = new LinkedHashMap<>();
catalog.put("Tablet", 35000.0);
catalog.put("Monitor", 15000.0);
catalog.forEach(printProduct);
}
}
4. Supplier<T> – Supply a Value
import java.util.function.Supplier;
import java.util.*;
public class SupplierExample {
public static void main(String[] args) {
Supplier<String> greeting = () -> "Hello, Java 8!";
Supplier<Double> random = Math::random;
Supplier<List<String>> listSupplier = ArrayList::new;
System.out.println(greeting.get()); // Hello, Java 8!
System.out.println(random.get()); // random number
System.out.println(listSupplier.get()); // []
// Lazy evaluation — Supplier defers computation until get() is called
Supplier<String> expensive = () -> {
System.out.println("Computing...");
return "Expensive Result";
};
System.out.println("Before get()");
String value = expensive.get(); // "Computing..." printed here
System.out.println(value);
// Practical: Optional.orElseGet() with Supplier — only called if empty
Optional<String> empty = Optional.empty();
String result = empty.orElseGet(() -> "Default from Supplier");
System.out.println(result); // Default from Supplier
// Factory pattern — Supplier as a factory
Supplier<java.time.LocalDateTime> now = java.time.LocalDateTime::now;
System.out.println(now.get());
// Wait a bit...
System.out.println(now.get()); // different time each call
}
}
Creating Custom Functional Interfaces
import java.util.function.*;
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
}
// Usage
TriFunction<Integer, Integer, Integer, Integer> sum3 =
(a, b, c) -> a + b + c;
System.out.println(sum3.apply(1, 2, 3)); // 6
@FunctionalInterface
interface Validator<T> {
boolean validate(T value);
default Validator<T> and(Validator<T> other) {
return value -> this.validate(value) && other.validate(value);
}
}
Validator<String> notEmpty = s -> !s.isEmpty();
Validator<String> notTooLong = s -> s.length() <= 20;
Validator<String> combined = notEmpty.and(notTooLong);
System.out.println(combined.validate("Hello")); // true
System.out.println(combined.validate("")); // false
System.out.println(combined.validate("This is way too long!!")); // false
Specialized Variants
| Interface | Avoids | Method |
|---|---|---|
IntPredicate, LongPredicate, DoublePredicate |
Integer/Long/Double boxing | test(int) |
IntFunction<R> |
Integer boxing on input | apply(int) |
ToIntFunction<T> |
Integer boxing on output | applyAsInt(T) |
UnaryOperator<T> |
N/A — extends Function<T,T> | apply(T) |
BinaryOperator<T> |
N/A — extends BiFunction<T,T,T> | apply(T,T) |
Summary
The four core functional interfaces cover every function shape: Predicate tests a condition, Function transforms input to output, Consumer processes input without returning, and Supplier produces a value without taking input. All four are used extensively in the Stream API — filter() takes a Predicate, map() takes a Function, forEach() takes a Consumer, and orElseGet() takes a Supplier. Learn these four interfaces and you can understand any lambda-based Java 8 code at a glance.
Comments