Imagine a contract that says: “You must do exactly one thing.” It does not care what that thing is — it could be filtering data, transforming text, printing output, or computing a value. But the contract only has one obligation. That is a functional interface in Java.
A functional interface is a Java interface that contains exactly one abstract method. This is often called a SAM (Single Abstract Method) interface. Because there is only one method to implement, the compiler can unambiguously match a lambda expression or method reference to that method.
Key concept: Functional interfaces are the bridge between object-oriented Java and functional programming. Every lambda expression in Java targets a functional interface. Without functional interfaces, lambdas would not exist in Java.
Important rules:
toString(), equals(), and hashCode() are inherited from Object and do not count toward the abstract method limit@FunctionalInterface annotation is optional but recommended — it tells the compiler to enforce the single-abstract-method rule// A valid functional interface -- exactly one abstract method
@FunctionalInterface
public interface Greeting {
String greet(String name); // the single abstract method
}
// Still functional -- default methods don't count
@FunctionalInterface
public interface Transformer {
String transform(String input); // the single abstract method
default String transformAndTrim(String input) {
return transform(input).trim();
}
default String transformAndUpperCase(String input) {
return transform(input).toUpperCase();
}
}
// Still functional -- static methods and Object methods don't count
@FunctionalInterface
public interface Validator {
boolean validate(String input); // the single abstract method
static Validator alwaysTrue() {
return input -> true;
}
// toString() comes from Object -- does NOT count
String toString();
}
// NOT functional -- two abstract methods (compiler error with @FunctionalInterface)
// @FunctionalInterface // would cause compile error
interface NotFunctional {
void methodA();
void methodB();
}
The @FunctionalInterface annotation serves two purposes:
You do not need @FunctionalInterface for an interface to work with lambdas. Any interface with exactly one abstract method will work. But adding the annotation is a best practice because it prevents accidental breakage.
@FunctionalInterface
interface MathOperation {
double calculate(double a, double b);
}
public class FunctionalInterfaceBasic {
public static void main(String[] args) {
// Lambda expressions implementing the same functional interface
MathOperation addition = (a, b) -> a + b;
MathOperation subtraction = (a, b) -> a - b;
MathOperation multiplication = (a, b) -> a * b;
MathOperation division = (a, b) -> b != 0 ? a / b : 0;
System.out.println("10 + 5 = " + addition.calculate(10, 5));
System.out.println("10 - 5 = " + subtraction.calculate(10, 5));
System.out.println("10 * 5 = " + multiplication.calculate(10, 5));
System.out.println("10 / 5 = " + division.calculate(10, 5));
// Output:
// 10 + 5 = 15.0
// 10 - 5 = 5.0
// 10 * 5 = 50.0
// 10 / 5 = 2.0
// Passing a lambda to a method that accepts the functional interface
printResult("Addition", addition, 20, 3);
printResult("Division", division, 20, 3);
// Output:
// Addition: 23.0
// Division: 6.666666666666667
}
static void printResult(String label, MathOperation op, double a, double b) {
System.out.println(label + ": " + op.calculate(a, b));
}
}
Functional interfaces are not new to Java 8 — the concept existed long before, just without the name or the annotation. Many classic Java interfaces happen to have exactly one abstract method, making them functional interfaces by definition. Java 8 simply gave them a formal name and made them compatible with lambda expressions.
| Interface | Package | Abstract Method | Used For |
|---|---|---|---|
Runnable |
java.lang |
void run() |
Running code in threads |
Callable<V> |
java.util.concurrent |
V call() |
Tasks that return a value |
Comparator<T> |
java.util |
int compare(T o1, T o2) |
Sorting and ordering |
ActionListener |
java.awt.event |
void actionPerformed(ActionEvent) |
GUI event handling |
FileFilter |
java.io |
boolean accept(File) |
Filtering files |
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class PreJava8FunctionalInterfaces {
public static void main(String[] args) {
// --- Runnable: Before and After ---
// Before Java 8: Anonymous inner class
Runnable oldRunnable = new Runnable() {
@Override
public void run() {
System.out.println("Running with anonymous class");
}
};
// Java 8+: Lambda expression
Runnable newRunnable = () -> System.out.println("Running with lambda");
new Thread(oldRunnable).start();
new Thread(newRunnable).start();
new Thread(() -> System.out.println("Running inline")).start();
// --- Comparator: Before and After ---
List names = Arrays.asList("Charlie", "Alice", "Bob");
// Before Java 8
Collections.sort(names, new Comparator() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
System.out.println("Old sort: " + names);
// Java 8+
List names2 = Arrays.asList("Charlie", "Alice", "Bob");
names2.sort((a, b) -> a.compareTo(b));
System.out.println("Lambda sort: " + names2);
// Even shorter with method reference
List names3 = Arrays.asList("Charlie", "Alice", "Bob");
names3.sort(String::compareTo);
System.out.println("Method ref sort: " + names3);
}
}
Java 8 introduced the java.util.function package, which contains 43 built-in functional interfaces. These cover the most common functional programming patterns so you rarely need to create your own. The four core interfaces are Predicate, Function, Consumer, and Supplier. Everything else is a variant of these four.
A Predicate takes one argument and returns a boolean. It represents a condition or test. Its abstract method is boolean test(T t).
Default methods: and(), or(), negate()
Static method: isEqual()
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateExample {
public static void main(String[] args) {
// --- Basic Predicate ---
Predicate isPositive = n -> n > 0;
Predicate isEven = n -> n % 2 == 0;
System.out.println("5 is positive: " + isPositive.test(5)); // true
System.out.println("-3 is positive: " + isPositive.test(-3)); // false
System.out.println("4 is even: " + isEven.test(4)); // true
// --- Predicate.and() -- both conditions must be true ---
Predicate isPositiveAndEven = isPositive.and(isEven);
System.out.println("4 is positive AND even: " + isPositiveAndEven.test(4)); // true
System.out.println("-2 is positive AND even: " + isPositiveAndEven.test(-2)); // false
// --- Predicate.or() -- at least one condition must be true ---
Predicate isPositiveOrEven = isPositive.or(isEven);
System.out.println("-2 is positive OR even: " + isPositiveOrEven.test(-2)); // true
System.out.println("-3 is positive OR even: " + isPositiveOrEven.test(-3)); // false
// --- Predicate.negate() -- reverse the condition ---
Predicate isNotPositive = isPositive.negate();
System.out.println("-5 is NOT positive: " + isNotPositive.test(-5)); // true
// --- Predicate.isEqual() -- static factory for equality check ---
Predicate isHello = Predicate.isEqual("Hello");
System.out.println("isHello(Hello): " + isHello.test("Hello")); // true
System.out.println("isHello(World): " + isHello.test("World")); // false
// --- Using Predicate with Streams ---
List numbers = Arrays.asList(-5, -2, 0, 3, 4, 7, 8, -1, 6);
List positiveEvens = numbers.stream()
.filter(isPositive.and(isEven))
.collect(Collectors.toList());
System.out.println("Positive evens: " + positiveEvens);
// Output: Positive evens: [4, 8, 6]
}
}
A Function takes one argument of type T and returns a result of type R. Its abstract method is R apply(T t). It represents a transformation or mapping.
Default methods: andThen(), compose()
Static method: identity()
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class FunctionExample {
public static void main(String[] args) {
// --- Basic Function ---
Function stringLength = String::length;
Function toUpper = String::toUpperCase;
Function intToString = n -> "Number: " + n;
System.out.println("Length of 'Hello': " + stringLength.apply("Hello")); // 5
System.out.println("Upper of 'hello': " + toUpper.apply("hello")); // HELLO
System.out.println(intToString.apply(42)); // Number: 42
// --- andThen() -- apply this function, THEN apply the next ---
// toUpper first, then stringLength
Function upperThenLength = toUpper.andThen(stringLength);
System.out.println("'hello' -> upper -> length: " + upperThenLength.apply("hello"));
// Output: 5
// --- compose() -- apply the OTHER function first, THEN this one ---
// stringLength is applied AFTER toUpper
Function composedLength = stringLength.compose(toUpper);
System.out.println("Composed: " + composedLength.apply("hello")); // 5
// Another compose example: trim first, then upper
Function trim = String::trim;
Function trimThenUpper = toUpper.compose(trim);
System.out.println("' hello ' -> trim -> upper: " + trimThenUpper.apply(" hello "));
// Output: HELLO
// --- Function.identity() -- returns the input unchanged ---
Function noChange = Function.identity();
System.out.println("Identity: " + noChange.apply("same")); // same
// --- Using Function with Streams ---
List names = Arrays.asList("alice", "bob", "charlie");
List formatted = names.stream()
.map(toUpper.andThen(s -> ">> " + s))
.collect(Collectors.toList());
System.out.println("Formatted: " + formatted);
// Output: Formatted: [>> ALICE, >> BOB, >> CHARLIE]
}
}
| Method | Execution Order | Read As |
|---|---|---|
f.andThen(g) |
Apply f first, then g |
“Do f, and then do g” |
f.compose(g) |
Apply g first, then f |
“f is composed of g applied first” |
Think of andThen as a pipeline (left to right) and compose as mathematical composition (right to left, like f(g(x))).
A Consumer takes one argument and returns nothing (void). Its abstract method is void accept(T t). It represents a side-effect operation: printing, logging, writing to a database, etc.
Default method: andThen()
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class ConsumerExample {
public static void main(String[] args) {
// --- Basic Consumer ---
Consumer printer = System.out::println;
Consumer yeller = s -> System.out.println(s.toUpperCase() + "!!!");
printer.accept("Hello"); // Output: Hello
yeller.accept("watch out"); // Output: WATCH OUT!!!
// --- andThen() -- chain consumers ---
Consumer printThenYell = printer.andThen(yeller);
printThenYell.accept("hello");
// Output:
// hello
// HELLO!!!
// --- Practical: log before processing ---
Consumer logger = s -> System.out.println("[LOG] Processing: " + s);
Consumer processor = s -> System.out.println("[RESULT] " + s.toUpperCase());
Consumer logAndProcess = logger.andThen(processor);
List items = Arrays.asList("apple", "banana", "cherry");
items.forEach(logAndProcess);
// Output:
// [LOG] Processing: apple
// [RESULT] APPLE
// [LOG] Processing: banana
// [RESULT] BANANA
// [LOG] Processing: cherry
// [RESULT] CHERRY
// --- Using Consumer with forEach ---
List numbers = Arrays.asList(1, 2, 3, 4, 5);
Consumer printSquare = n -> System.out.println(n + " squared = " + (n * n));
numbers.forEach(printSquare);
// Output:
// 1 squared = 1
// 2 squared = 4
// 3 squared = 9
// 4 squared = 16
// 5 squared = 25
}
}
A Supplier takes no arguments and returns a value. Its abstract method is T get(). It represents a factory or lazy value provider. Suppliers are commonly used for deferred computation — the value is not computed until get() is called.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class SupplierExample {
public static void main(String[] args) {
// --- Basic Supplier ---
Supplier helloSupplier = () -> "Hello, World!";
Supplier randomSupplier = Math::random;
Supplier> listFactory = ArrayList::new;
System.out.println(helloSupplier.get()); // Hello, World!
System.out.println(randomSupplier.get()); // 0.7234... (random)
// --- Lazy evaluation ---
// The value is not computed until get() is called
Supplier timestamp = System::currentTimeMillis;
System.out.println("Before: " + timestamp.get());
// simulate work
for (int i = 0; i < 1000000; i++) { /* busy work */ }
System.out.println("After: " + timestamp.get());
// The two timestamps will be different
// --- Factory pattern with Supplier ---
Supplier sbFactory = StringBuilder::new;
StringBuilder sb1 = sbFactory.get(); // new instance each time
StringBuilder sb2 = sbFactory.get(); // another new instance
sb1.append("First");
sb2.append("Second");
System.out.println(sb1); // First
System.out.println(sb2); // Second
// --- Generate infinite stream with Supplier ---
Random random = new Random(42);
Supplier diceRoll = () -> random.nextInt(6) + 1;
List rolls = Stream.generate(diceRoll)
.limit(10)
.collect(Collectors.toList());
System.out.println("Dice rolls: " + rolls);
// Output: Dice rolls: [1, 5, 3, 2, 4, 2, 6, 3, 1, 4] (varies with seed)
// --- Supplier with Optional.orElseGet ---
String name = null;
Supplier defaultName = () -> "Unknown User";
String result = java.util.Optional.ofNullable(name)
.orElseGet(defaultName);
System.out.println("Result: " + result);
// Output: Result: Unknown User
}
}
UnaryOperator<T> is a specialization of Function<T, T> where the input and output types are the same. BinaryOperator<T> is a specialization of BiFunction<T, T, T> where all three types are the same.
| Interface | Extends | Abstract Method | Use Case |
|---|---|---|---|
UnaryOperator<T> |
Function<T, T> |
T apply(T t) |
Transform a value to the same type |
BinaryOperator<T> |
BiFunction<T, T, T> |
T apply(T t1, T t2) |
Combine two values of the same type |
import java.util.Arrays;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
public class OperatorExample {
public static void main(String[] args) {
// --- UnaryOperator: same input and output type ---
UnaryOperator toUpper = String::toUpperCase;
UnaryOperator addBrackets = s -> "[" + s + "]";
UnaryOperator doubleIt = n -> n * 2;
System.out.println(toUpper.apply("hello")); // HELLO
System.out.println(addBrackets.apply("java")); // [java]
System.out.println(doubleIt.apply(5)); // 10
// --- Chaining UnaryOperators ---
UnaryOperator upperAndBracket = s -> addBrackets.apply(toUpper.apply(s));
System.out.println(upperAndBracket.apply("hello")); // [HELLO]
// --- Using UnaryOperator with List.replaceAll ---
List names = Arrays.asList("alice", "bob", "charlie");
names.replaceAll(String::toUpperCase);
System.out.println("ReplaceAll: " + names);
// Output: ReplaceAll: [ALICE, BOB, CHARLIE]
// --- BinaryOperator: combine two values of the same type ---
BinaryOperator add = Integer::sum;
BinaryOperator max = Integer::max;
BinaryOperator concat = String::concat;
System.out.println("Sum: " + add.apply(10, 20)); // 30
System.out.println("Max: " + max.apply(10, 20)); // 20
System.out.println("Concat: " + concat.apply("Hello, ", "World!")); // Hello, World!
// --- BinaryOperator with Stream.reduce ---
List numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Stream sum: " + sum);
// Output: Stream sum: 15
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println("Stream product: " + product);
// Output: Stream product: 120
// --- BinaryOperator.maxBy and minBy ---
BinaryOperator longerString = BinaryOperator.maxBy(
java.util.Comparator.comparingInt(String::length));
System.out.println("Longer: " + longerString.apply("hello", "hi"));
// Output: Longer: hello
}
}
The “Bi” variants accept two parameters instead of one. They mirror the single-parameter interfaces but with an extra input.
| Interface | Abstract Method | Single-Param Equivalent |
|---|---|---|
BiFunction<T, U, R> |
R apply(T t, U u) |
Function<T, R> |
BiPredicate<T, U> |
boolean test(T t, U u) |
Predicate<T> |
BiConsumer<T, U> |
void accept(T t, U u) |
Consumer<T> |
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
public class BiVariantExample {
public static void main(String[] args) {
// --- BiFunction: two inputs, one output ---
BiFunction repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("Ha", 3));
// Output: HaHaHa
BiFunction power = Math::pow;
System.out.println("2^10 = " + power.apply(2.0, 10.0));
// Output: 2^10 = 1024.0
// --- BiPredicate: two inputs, boolean output ---
BiPredicate longerThan = (s, len) -> s.length() > len;
System.out.println("'hello' longer than 3: " + longerThan.test("hello", 3)); // true
System.out.println("'hi' longer than 3: " + longerThan.test("hi", 3)); // false
// Combining BiPredicates
BiPredicate shorterThan10 = (s, len) -> s.length() < 10;
BiPredicate validLength = longerThan.and(shorterThan10);
System.out.println("'hello' valid (>3 and <10): " + validLength.test("hello", 3)); // true
// --- BiConsumer: two inputs, no output ---
BiConsumer printEntry = (key, value) ->
System.out.println(key + " = " + value);
printEntry.accept("Score", 95);
// Output: Score = 95
// BiConsumer with Map.forEach
Map scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
System.out.println("--- Scores ---");
scores.forEach((name, score) ->
System.out.println(name + ": " + score + (score >= 90 ? " (A)" : " (B)")));
// Output:
// --- Scores ---
// Alice: 95 (A)
// Bob: 87 (B)
// Charlie: 92 (A)
}
}
Generics in Java cannot use primitive types (int, long, double). When you use Function<Integer, Integer>, every int value gets autoboxed into an Integer object, which hurts performance. To solve this, java.util.function provides primitive specializations that work directly with int, long, and double — avoiding autoboxing entirely.
| Category | Primitive Specializations |
|---|---|
| Predicate | IntPredicate, LongPredicate, DoublePredicate |
| Function | IntFunction<R>, LongFunction<R>, DoubleFunction<R> |
| To-type Function | ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> |
| Type-to-type Function | IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction, DoubleToIntFunction, DoubleToLongFunction |
| Consumer | IntConsumer, LongConsumer, DoubleConsumer |
| Supplier | IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier |
| UnaryOperator | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
| BinaryOperator | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
| Bi-variant | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> |
import java.util.function.*;
import java.util.stream.IntStream;
public class PrimitiveSpecializationExample {
public static void main(String[] args) {
// --- IntPredicate: avoids autoboxing int to Integer ---
IntPredicate isEven = n -> n % 2 == 0;
IntPredicate isPositive = n -> n > 0;
System.out.println("4 is even: " + isEven.test(4)); // true
System.out.println("4 is positive AND even: " +
isPositive.and(isEven).test(4)); // true
// --- IntFunction: int input, generic output ---
IntFunction intToLabel = n -> "Item #" + n;
System.out.println(intToLabel.apply(5));
// Output: Item #5
// --- ToIntFunction: generic input, int output ---
ToIntFunction stringLen = String::length;
System.out.println("Length of 'hello': " + stringLen.applyAsInt("hello"));
// Output: Length of 'hello': 5
// --- IntUnaryOperator: int to int ---
IntUnaryOperator doubleIt = n -> n * 2;
IntUnaryOperator addTen = n -> n + 10;
// Chain: double first, then add 10
IntUnaryOperator doubleThenAdd = doubleIt.andThen(addTen);
System.out.println("5 -> double -> add10: " + doubleThenAdd.applyAsInt(5));
// Output: 5 -> double -> add10: 20
// --- IntBinaryOperator: two ints, one int result ---
IntBinaryOperator max = Integer::max;
System.out.println("Max of 3 and 7: " + max.applyAsInt(3, 7));
// Output: Max of 3 and 7: 7
// --- Performance benefit with IntStream ---
int sum = IntStream.rangeClosed(1, 100)
.filter(isEven) // IntPredicate -- no autoboxing
.sum();
System.out.println("Sum of even numbers 1-100: " + sum);
// Output: Sum of even numbers 1-100: 2550
}
}
One of the most powerful features of functional interfaces is composition — combining simple functions into complex behavior. This is the essence of functional programming: building big things from small, reusable pieces.
Predicates can be combined using and(), or(), and negate() to build complex conditions from simple ones.
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateComposition {
public static void main(String[] args) {
// Individual predicates
Predicate isNotNull = s -> s != null;
Predicate isNotEmpty = s -> !s.isEmpty();
Predicate isNotBlank = s -> !s.isBlank();
Predicate startsWithJ = s -> s.startsWith("J");
Predicate longerThan3 = s -> s.length() > 3;
// Compose a complex validation predicate
Predicate isValidJavaName = isNotNull
.and(isNotEmpty)
.and(isNotBlank)
.and(startsWithJ)
.and(longerThan3);
List candidates = Arrays.asList(
"Java", "JavaScript", null, "", " ", "J", "Jolt",
"Python", "Jakarta", "JUnit"
);
List valid = candidates.stream()
.filter(isValidJavaName)
.collect(Collectors.toList());
System.out.println("Valid Java names: " + valid);
// Output: Valid Java names: [Java, JavaScript, Jolt, Jakarta, JUnit]
// Reuse individual predicates in different combinations
Predicate isValidNonJava = isNotNull
.and(isNotEmpty)
.and(startsWithJ.negate()) // does NOT start with J
.and(longerThan3);
List nonJava = candidates.stream()
.filter(isValidNonJava)
.collect(Collectors.toList());
System.out.println("Valid non-Java names: " + nonJava);
// Output: Valid non-Java names: [Python]
}
}
Functions can be chained using andThen() (left to right) or compose() (right to left) to create transformation pipelines.
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class FunctionComposition {
public static void main(String[] args) {
// Individual transformation steps
Function trim = String::trim;
Function toUpper = String::toUpperCase;
Function addPrefix = s -> ">> " + s;
Function addSuffix = s -> s + " <<";
// Build a pipeline: trim -> uppercase -> add prefix -> add suffix
Function pipeline = trim
.andThen(toUpper)
.andThen(addPrefix)
.andThen(addSuffix);
System.out.println(pipeline.apply(" hello world "));
// Output: >> HELLO WORLD <<
// Apply pipeline to a list
List rawInputs = Arrays.asList(" alice ", " bob ", " charlie ");
List processed = rawInputs.stream()
.map(pipeline)
.collect(Collectors.toList());
System.out.println(processed);
// Output: [>> ALICE <<, >> BOB <<, >> CHARLIE <<]
// Compose: build a transformation and then convert to length
Function processedLength = pipeline.andThen(String::length);
rawInputs.stream()
.map(processedLength)
.forEach(len -> System.out.println("Length: " + len));
// Output:
// Length: 14
// Length: 12
// Length: 16
}
}
Consumers can be chained using andThen() to perform multiple side-effect actions in sequence. This is useful for logging, auditing, and multi-step processing.
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import java.util.function.Consumer;
public class ConsumerComposition {
public static void main(String[] args) {
// Individual consumers
Consumer log = s -> System.out.println("[LOG] " + s);
Consumer validate = s -> {
if (s.length() < 2) {
System.out.println("[WARN] '" + s + "' is too short");
}
};
Consumer process = s -> System.out.println("[PROCESS] " + s.toUpperCase());
// Chain: log -> validate -> process
Consumer fullPipeline = log.andThen(validate).andThen(process);
List inputs = Arrays.asList("hello", "a", "java", "b", "streams");
System.out.println("=== Processing Pipeline ===");
inputs.forEach(fullPipeline);
// Output:
// === Processing Pipeline ===
// [LOG] hello
// [PROCESS] HELLO
// [LOG] a
// [WARN] 'a' is too short
// [PROCESS] A
// [LOG] java
// [PROCESS] JAVA
// [LOG] b
// [WARN] 'b' is too short
// [PROCESS] B
// [LOG] streams
// [PROCESS] STREAMS
// Practical: collecting and printing in one pass
List collected = new ArrayList<>();
Consumer collectAndPrint = ((Consumer) collected::add)
.andThen(s -> System.out.println("Added: " + s));
Arrays.asList("X", "Y", "Z").forEach(collectAndPrint);
System.out.println("Collected: " + collected);
// Output:
// Added: X
// Added: Y
// Added: Z
// Collected: [X, Y, Z]
}
}
Java’s built-in functional interfaces cover most common patterns: one input (Function), two inputs (BiFunction), no output (Consumer), no input (Supplier), and boolean output (Predicate). But sometimes you need something that does not fit, such as a function with three parameters or a function that throws a checked exception.
import java.util.function.Function; // Custom functional interface for three parameters @FunctionalInterface interface TriFunction { R apply(A a, B b, C c); // Default method for chaining defaultTriFunction andThen(Function super R, ? extends V> after) { return (a, b, c) -> after.apply(apply(a, b, c)); } } public class CustomTriFunction { public static void main(String[] args) { // Three-parameter function: calculate total price TriFunction orderSummary = (product, quantity, price) -> String.format("%s x%d = $%.2f", product, quantity, quantity * price); System.out.println(orderSummary.apply("Widget", 5, 9.99)); // Output: Widget x5 = $49.95 System.out.println(orderSummary.apply("Gadget", 3, 24.50)); // Output: Gadget x3 = $73.50 // Using andThen to chain TriFunction summaryLength = orderSummary.andThen(String::length); System.out.println("Summary length: " + summaryLength.apply("Widget", 5, 9.99)); // Output: Summary length: 19 // Three-parameter function: full name formatter TriFunction fullName = (first, middle, last) -> first + " " + middle + " " + last; System.out.println(fullName.apply("John", "Michael", "Smith")); // Output: John Michael Smith } }
Java’s built-in functional interfaces do not declare checked exceptions. This means you cannot use a lambda that throws IOException or SQLException with Function<T, R>. The solution is a custom functional interface with a throws clause.
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; // Functional interface that allows checked exceptions @FunctionalInterface interface CheckedFunction{ R apply(T t) throws Exception; } public class CheckedExceptionInterface { // Utility method to wrap CheckedFunction into a standard Function static java.util.function.Function unchecked(CheckedFunction f) { return t -> { try { return f.apply(t); } catch (Exception e) { throw new RuntimeException(e); } }; } // A method that throws a checked exception static Integer parseStrictly(String s) throws Exception { if (s == null || s.isBlank()) { throw new Exception("Input cannot be null or blank"); } return Integer.parseInt(s.trim()); } public static void main(String[] args) { List inputs = Arrays.asList("1", "2", "3", "4", "5"); // Without the wrapper, this would not compile because // parseStrictly throws a checked Exception. // With unchecked(), we wrap it into a standard Function. List numbers = inputs.stream() .map(unchecked(CheckedExceptionInterface::parseStrictly)) .collect(Collectors.toList()); System.out.println("Parsed: " + numbers); // Output: Parsed: [1, 2, 3, 4, 5] } }
Sometimes a custom interface with a meaningful name is clearer than a generic Function<User, Boolean>. Domain-specific names make the code self-documenting.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class DomainSpecificInterface {
// Domain-specific functional interfaces
@FunctionalInterface
interface Converter {
T convert(F from);
}
@FunctionalInterface
interface Validator {
boolean isValid(T item);
default Validator and(Validator other) {
return item -> isValid(item) && other.isValid(item);
}
default Validator or(Validator other) {
return item -> isValid(item) || other.isValid(item);
}
default Validator negate() {
return item -> !isValid(item);
}
}
static class User {
String name;
int age;
String email;
User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public String toString() {
return name + " (age " + age + ", " + email + ")";
}
}
public static void main(String[] args) {
// Converter usage -- clear intent
Converter stringToInt = Integer::parseInt;
Converter csvToUser = csv -> {
String[] parts = csv.split(",");
return new User(parts[0].trim(), Integer.parseInt(parts[1].trim()), parts[2].trim());
};
System.out.println(stringToInt.convert("42"));
// Output: 42
User user = csvToUser.convert("Alice, 30, alice@example.com");
System.out.println(user);
// Output: Alice (age 30, alice@example.com)
// Validator usage -- composable and domain-meaningful
Validator hasName = u -> u.name != null && !u.name.isBlank();
Validator isAdult = u -> u.age >= 18;
Validator hasEmail = u -> u.email != null && u.email.contains("@");
Validator isValidUser = hasName.and(isAdult).and(hasEmail);
List users = Arrays.asList(
new User("Alice", 30, "alice@example.com"),
new User("", 25, "bob@example.com"),
new User("Charlie", 15, "charlie@example.com"),
new User("Diana", 28, null)
);
List validUsers = users.stream()
.filter(isValidUser::isValid)
.collect(Collectors.toList());
System.out.println("Valid users: " + validUsers);
// Output: Valid users: [Alice (age 30, alice@example.com)]
}
}
This table lists every functional interface in java.util.function, organized by category. Use it as a quick reference when deciding which interface to use.
| Interface | Abstract Method | Input | Output | Use Case |
|---|---|---|---|---|
Predicate<T> |
boolean test(T) |
T | boolean | Filtering, conditions |
Function<T, R> |
R apply(T) |
T | R | Transformations, mapping |
Consumer<T> |
void accept(T) |
T | void | Side effects (print, save) |
Supplier<T> |
T get() |
none | T | Factories, lazy values |
UnaryOperator<T> |
T apply(T) |
T | T | Same-type transformation |
BinaryOperator<T> |
T apply(T, T) |
T, T | T | Reducing, combining |
| Interface | Abstract Method | Input | Output | Use Case |
|---|---|---|---|---|
BiPredicate<T, U> |
boolean test(T, U) |
T, U | boolean | Two-input conditions |
BiFunction<T, U, R> |
R apply(T, U) |
T, U | R | Two-input transformations |
BiConsumer<T, U> |
void accept(T, U) |
T, U | void | Map.forEach, two-arg actions |
| Interface | Abstract Method | Avoids Boxing |
|---|---|---|
IntPredicate |
boolean test(int) |
Predicate<Integer> |
LongPredicate |
boolean test(long) |
Predicate<Long> |
DoublePredicate |
boolean test(double) |
Predicate<Double> |
| Interface | Abstract Method | Description |
|---|---|---|
IntFunction<R> |
R apply(int) |
int input, generic output |
LongFunction<R> |
R apply(long) |
long input, generic output |
DoubleFunction<R> |
R apply(double) |
double input, generic output |
ToIntFunction<T> |
int applyAsInt(T) |
generic input, int output |
ToLongFunction<T> |
long applyAsLong(T) |
generic input, long output |
ToDoubleFunction<T> |
double applyAsDouble(T) |
generic input, double output |
IntToLongFunction |
long applyAsLong(int) |
int to long |
IntToDoubleFunction |
double applyAsDouble(int) |
int to double |
LongToIntFunction |
int applyAsInt(long) |
long to int |
LongToDoubleFunction |
double applyAsDouble(long) |
long to double |
DoubleToIntFunction |
int applyAsInt(double) |
double to int |
DoubleToLongFunction |
long applyAsLong(double) |
double to long |
| Interface | Abstract Method | Description |
|---|---|---|
IntConsumer |
void accept(int) |
Consumes int without boxing |
LongConsumer |
void accept(long) |
Consumes long without boxing |
DoubleConsumer |
void accept(double) |
Consumes double without boxing |
ObjIntConsumer<T> |
void accept(T, int) |
Object + int consumer |
ObjLongConsumer<T> |
void accept(T, long) |
Object + long consumer |
ObjDoubleConsumer<T> |
void accept(T, double) |
Object + double consumer |
IntSupplier |
int getAsInt() |
Supplies int without boxing |
LongSupplier |
long getAsLong() |
Supplies long without boxing |
DoubleSupplier |
double getAsDouble() |
Supplies double without boxing |
BooleanSupplier |
boolean getAsBoolean() |
Supplies boolean without boxing |
| Interface | Abstract Method | Description |
|---|---|---|
IntUnaryOperator |
int applyAsInt(int) |
int to int |
LongUnaryOperator |
long applyAsLong(long) |
long to long |
DoubleUnaryOperator |
double applyAsDouble(double) |
double to double |
IntBinaryOperator |
int applyAsInt(int, int) |
Two ints to int |
LongBinaryOperator |
long applyAsLong(long, long) |
Two longs to long |
DoubleBinaryOperator |
double applyAsDouble(double, double) |
Two doubles to double |
Let us build a reusable validation framework using Predicate composition. This demonstrates how functional interfaces enable elegant, composable designs that would require far more code with traditional OOP approaches.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class ValidationFramework {
// --- ValidationResult: holds success/failure and messages ---
static class ValidationResult {
private final boolean valid;
private final List errors;
private ValidationResult(boolean valid, List errors) {
this.valid = valid;
this.errors = errors;
}
static ValidationResult success() {
return new ValidationResult(true, List.of());
}
static ValidationResult failure(String error) {
return new ValidationResult(false, List.of(error));
}
static ValidationResult combine(ValidationResult a, ValidationResult b) {
if (a.valid && b.valid) return success();
List allErrors = new ArrayList<>(a.errors);
allErrors.addAll(b.errors);
return new ValidationResult(false, allErrors);
}
boolean isValid() { return valid; }
List getErrors() { return errors; }
@Override
public String toString() {
return valid ? "VALID" : "INVALID: " + errors;
}
}
// --- Validator: wraps a Predicate with an error message ---
@FunctionalInterface
interface Validator {
ValidationResult validate(T t);
default Validator and(Validator other) {
return t -> ValidationResult.combine(this.validate(t), other.validate(t));
}
// Factory method: create a Validator from a Predicate and error message
static Validator of(Predicate predicate, String errorMessage) {
return t -> predicate.test(t)
? ValidationResult.success()
: ValidationResult.failure(errorMessage);
}
}
// --- Domain class ---
static class RegistrationForm {
String username;
String email;
String password;
int age;
RegistrationForm(String username, String email, String password, int age) {
this.username = username;
this.email = email;
this.password = password;
this.age = age;
}
@Override
public String toString() {
return "Form{user='" + username + "', email='" + email + "', age=" + age + "}";
}
}
public static void main(String[] args) {
// --- Build individual validators using Predicate composition ---
Validator usernameNotEmpty = Validator.of(
f -> f.username != null && !f.username.isBlank(),
"Username is required"
);
Validator usernameMinLength = Validator.of(
f -> f.username != null && f.username.length() >= 3,
"Username must be at least 3 characters"
);
Validator usernameMaxLength = Validator.of(
f -> f.username == null || f.username.length() <= 20,
"Username must be at most 20 characters"
);
Validator emailValid = Validator.of(
f -> f.email != null && f.email.matches("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"),
"Email is invalid"
);
Validator passwordStrong = Validator.of(
f -> f.password != null && f.password.length() >= 8,
"Password must be at least 8 characters"
);
Validator passwordHasDigit = Validator.of(
f -> f.password != null && f.password.chars().anyMatch(Character::isDigit),
"Password must contain at least one digit"
);
Validator passwordHasUpper = Validator.of(
f -> f.password != null && f.password.chars().anyMatch(Character::isUpperCase),
"Password must contain at least one uppercase letter"
);
Validator ageValid = Validator.of(
f -> f.age >= 13 && f.age <= 120,
"Age must be between 13 and 120"
);
// --- Compose all validators into one ---
Validator fullValidator = usernameNotEmpty
.and(usernameMinLength)
.and(usernameMaxLength)
.and(emailValid)
.and(passwordStrong)
.and(passwordHasDigit)
.and(passwordHasUpper)
.and(ageValid);
// --- Test data ---
List forms = Arrays.asList(
new RegistrationForm("Alice", "alice@example.com", "SecurePass1", 25),
new RegistrationForm("Bo", "bo@x.com", "weakpw", 10),
new RegistrationForm("", "not-an-email", "12345678", 30),
new RegistrationForm("ValidUser", "user@site.org", "MyP@ssw0rd", 45)
);
// --- Validate each form ---
System.out.println("=== Registration Validation Results ===\n");
for (RegistrationForm form : forms) {
ValidationResult result = fullValidator.validate(form);
System.out.println(form);
System.out.println(" Result: " + result);
System.out.println();
}
// Output:
// === Registration Validation Results ===
//
// Form{user='Alice', email='alice@example.com', age=25}
// Result: VALID
//
// Form{user='Bo', email='bo@x.com', age=10}
// Result: INVALID: [Username must be at least 3 characters, Password must be at least 8 characters, Password must contain at least one uppercase letter, Age must be between 13 and 120]
//
// Form{user='', email='not-an-email', age=30}
// Result: INVALID: [Username is required, Username must be at least 3 characters, Email is invalid, Password must contain at least one uppercase letter]
//
// Form{user='ValidUser', email='user@site.org', age=45}
// Result: VALID
// --- Filter only valid forms ---
List validForms = forms.stream()
.filter(f -> fullValidator.validate(f).isValid())
.collect(Collectors.toList());
System.out.println("Valid forms: " + validForms);
// Output: Valid forms: [Form{user='Alice'...}, Form{user='ValidUser'...}]
// --- Transform valid forms ---
Function toWelcomeMessage =
f -> "Welcome, " + f.username + "! Your account (" + f.email + ") has been created.";
validForms.stream()
.map(toWelcomeMessage)
.forEach(System.out::println);
// Output:
// Welcome, Alice! Your account (alice@example.com) has been created.
// Welcome, ValidUser! Your account (user@site.org) has been created.
}
}
This validation framework demonstrates several functional interface concepts working together:
| Concept | Where Used | How |
|---|---|---|
| Predicate | Validator.of(predicate, message) |
Each validation rule is a Predicate |
| Composition (and) | validator1.and(validator2) |
Combines validators into a single check |
| Custom Functional Interface | Validator<T> |
Wraps Predicate with error messages |
| Function | toWelcomeMessage |
Transforms valid forms into strings |
| Method Reference | Character::isDigit, System.out::println |
Clean stream operations |
| Static Factory | Validator.of() |
Creates validators declaratively |
@FunctionalInterface annotation is optional but recommended — it prevents accidental breakage by enforcing the single-method rule at compile time.Runnable, Comparator, Callable). Java 8 formalized the concept and added the java.util.function package.and()/or()/negate(), functions with andThen()/compose(), and consumers with andThen().