1. What is a Functional Interface?
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:
- Exactly one abstract method — this is the defining rule
- Default methods do not count — an interface can have any number of default methods and still be functional
- Static methods do not count — static methods in interfaces are not abstract
- Methods from Object do not count —
toString(),equals(), andhashCode()are inherited from Object and do not count toward the abstract method limit - The
@FunctionalInterfaceannotation 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();
}1.1 The @FunctionalInterface Annotation
The @FunctionalInterface annotation serves two purposes:
- Documentation — It signals to other developers that this interface is designed to be used with lambdas.
- Compiler enforcement — If someone accidentally adds a second abstract method, the compiler will report an error.
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.
1.2 Using a Functional Interface with Lambdas
@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));
}
}2. Pre-Java 8 Functional Interfaces
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<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// Before Java 8
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
System.out.println("Old sort: " + names);
// Java 8+
List<String> 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<String> names3 = Arrays.asList("Charlie", "Alice", "Bob");
names3.sort(String::compareTo);
System.out.println("Method ref sort: " + names3);
}
}3. The java.util.function Package
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.
3.1 Predicate<T> — Test a Condition
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<Integer> isPositive = n -> n > 0;
Predicate<Integer> 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<Integer> 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<Integer> 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<Integer> isNotPositive = isPositive.negate();
System.out.println("-5 is NOT positive: " + isNotPositive.test(-5)); // true
// --- Predicate.isEqual() -- static factory for equality check ---
Predicate<String> 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<Integer> numbers = Arrays.asList(-5, -2, 0, 3, 4, 7, 8, -1, 6);
List<Integer> positiveEvens = numbers.stream()
.filter(isPositive.and(isEven))
.collect(Collectors.toList());
System.out.println("Positive evens: " + positiveEvens);
// Output: Positive evens: [4, 8, 6]
}
}3.2 Function<T, R> — Transform Input to Output
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<String, Integer> stringLength = String::length;
Function<String, String> toUpper = String::toUpperCase;
Function<Integer, String> 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<String, Integer> 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<String, Integer> composedLength = stringLength.compose(toUpper);
System.out.println("Composed: " + composedLength.apply("hello")); // 5
// Another compose example: trim first, then upper
Function<String, String> trim = String::trim;
Function<String, String> trimThenUpper = toUpper.compose(trim);
System.out.println("' hello ' -> trim -> upper: " + trimThenUpper.apply(" hello "));
// Output: HELLO
// --- Function.identity() -- returns the input unchanged ---
Function<String, String> noChange = Function.identity();
System.out.println("Identity: " + noChange.apply("same")); // same
// --- Using Function with Streams ---
List<String> names = Arrays.asList("alice", "bob", "charlie");
List<String> formatted = names.stream()
.map(toUpper.andThen(s -> ">> " + s))
.collect(Collectors.toList());
System.out.println("Formatted: " + formatted);
// Output: Formatted: [>> ALICE, >> BOB, >> CHARLIE]
}
}andThen() vs compose() — Visual Explanation
| 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))).
3.3 Consumer<T> — Perform an Action
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<String> printer = System.out::println;
Consumer<String> yeller = s -> System.out.println(s.toUpperCase() + "!!!");
printer.accept("Hello"); // Output: Hello
yeller.accept("watch out"); // Output: WATCH OUT!!!
// --- andThen() -- chain consumers ---
Consumer<String> printThenYell = printer.andThen(yeller);
printThenYell.accept("hello");
// Output:
// hello
// HELLO!!!
// --- Practical: log before processing ---
Consumer<String> logger = s -> System.out.println("[LOG] Processing: " + s);
Consumer<String> processor = s -> System.out.println("[RESULT] " + s.toUpperCase());
Consumer<String> logAndProcess = logger.andThen(processor);
List<String> 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<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Consumer<Integer> 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
}
}3.4 Supplier<T> — Provide a Value
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<String> helloSupplier = () -> "Hello, World!";
Supplier<Double> randomSupplier = Math::random;
Supplier<List<String>> 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<Long> 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<StringBuilder> 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<Integer> diceRoll = () -> random.nextInt(6) + 1;
List<Integer> 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<String> defaultName = () -> "Unknown User";
String result = java.util.Optional.ofNullable(name)
.orElseGet(defaultName);
System.out.println("Result: " + result);
// Output: Result: Unknown User
}
}3.5 UnaryOperator<T> and BinaryOperator<T>
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<String> toUpper = String::toUpperCase;
UnaryOperator<String> addBrackets = s -> "[" + s + "]";
UnaryOperator<Integer> 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<String> upperAndBracket = s -> addBrackets.apply(toUpper.apply(s));
System.out.println(upperAndBracket.apply("hello")); // [HELLO]
// --- Using UnaryOperator with List.replaceAll ---
List<String> 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<Integer> add = Integer::sum;
BinaryOperator<Integer> max = Integer::max;
BinaryOperator<String> 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<Integer> 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<String> longerString = BinaryOperator.maxBy(
java.util.Comparator.comparingInt(String::length));
System.out.println("Longer: " + longerString.apply("hello", "hi"));
// Output: Longer: hello
}
}3.6 Bi-Variant Interfaces
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<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("Ha", 3));
// Output: HaHaHa
BiFunction<Double, Double, Double> 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<String, Integer> 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<String, Integer> shorterThan10 = (s, len) -> s.length() < 10;
BiPredicate<String, Integer> validLength = longerThan.and(shorterThan10);
System.out.println("'hello' valid (>3 and <10): " + validLength.test("hello", 3)); // true
// --- BiConsumer: two inputs, no output ---
BiConsumer<String, Integer> printEntry = (key, value) ->
System.out.println(key + " = " + value);
printEntry.accept("Score", 95);
// Output: Score = 95
// BiConsumer with Map.forEach
Map<String, Integer> 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)
}
}3.7 Primitive Specializations
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<String> intToLabel = n -> "Item #" + n;
System.out.println(intToLabel.apply(5));
// Output: Item #5
// --- ToIntFunction: generic input, int output ---
ToIntFunction<String> 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
}
}4. Composing Functional Interfaces
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.
4.1 Chaining Predicates
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<String> isNotNull = s -> s != null;
Predicate<String> isNotEmpty = s -> !s.isEmpty();
Predicate<String> isNotBlank = s -> !s.isBlank();
Predicate<String> startsWithJ = s -> s.startsWith("J");
Predicate<String> longerThan3 = s -> s.length() > 3;
// Compose a complex validation predicate
Predicate<String> isValidJavaName = isNotNull
.and(isNotEmpty)
.and(isNotBlank)
.and(startsWithJ)
.and(longerThan3);
List<String> candidates = Arrays.asList(
"Java", "JavaScript", null, "", " ", "J", "Jolt",
"Python", "Jakarta", "JUnit"
);
List<String> 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<String> isValidNonJava = isNotNull
.and(isNotEmpty)
.and(startsWithJ.negate()) // does NOT start with J
.and(longerThan3);
List<String> nonJava = candidates.stream()
.filter(isValidNonJava)
.collect(Collectors.toList());
System.out.println("Valid non-Java names: " + nonJava);
// Output: Valid non-Java names: [Python]
}
}4.2 Composing Functions
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<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> ">> " + s;
Function<String, String> addSuffix = s -> s + " <<";
// Build a pipeline: trim -> uppercase -> add prefix -> add suffix
Function<String, String> pipeline = trim
.andThen(toUpper)
.andThen(addPrefix)
.andThen(addSuffix);
System.out.println(pipeline.apply(" hello world "));
// Output: >> HELLO WORLD <<
// Apply pipeline to a list
List<String> rawInputs = Arrays.asList(" alice ", " bob ", " charlie ");
List<String> 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<String, Integer> processedLength = pipeline.andThen(String::length);
rawInputs.stream()
.map(processedLength)
.forEach(len -> System.out.println("Length: " + len));
// Output:
// Length: 14
// Length: 12
// Length: 16
}
}4.3 Consumer Chains
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<String> log = s -> System.out.println("[LOG] " + s);
Consumer<String> validate = s -> {
if (s.length() < 2) {
System.out.println("[WARN] '" + s + "' is too short");
}
};
Consumer<String> process = s -> System.out.println("[PROCESS] " + s.toUpperCase());
// Chain: log -> validate -> process
Consumer<String> fullPipeline = log.andThen(validate).andThen(process);
List<String> 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<String> collected = new ArrayList<>();
Consumer<String> collectAndPrint = ((Consumer<String>) 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]
}
}5. Creating Custom Functional Interfaces
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.
5.1 When to Create a Custom Functional Interface
- You need more than two parameters (no TriFunction in the JDK)
- You need a function that throws a checked exception
- You want a domain-specific name that reads better than generic types
- You need specific default methods for composition
5.2 TriFunction Example
import java.util.function.Function;
// Custom functional interface for three parameters
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
// Default method for chaining
default <V> TriFunction<A, B, C, V> 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<String, Integer, Double, String> 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<String, Integer, Double, Integer> 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<String, String, String, String> fullName =
(first, middle, last) -> first + " " + middle + " " + last;
System.out.println(fullName.apply("John", "Michael", "Smith"));
// Output: John Michael Smith
}
}5.3 Checked Exception Functional Interface
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<T, R> {
R apply(T t) throws Exception;
}
public class CheckedExceptionInterface {
// Utility method to wrap CheckedFunction into a standard Function
static <T, R> java.util.function.Function<T, R> unchecked(CheckedFunction<T, R> 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<String> 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<Integer> numbers = inputs.stream()
.map(unchecked(CheckedExceptionInterface::parseStrictly))
.collect(Collectors.toList());
System.out.println("Parsed: " + numbers);
// Output: Parsed: [1, 2, 3, 4, 5]
}
}5.4 Domain-Specific Functional Interface
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<F, T> {
T convert(F from);
}
@FunctionalInterface
interface Validator<T> {
boolean isValid(T item);
default Validator<T> and(Validator<T> other) {
return item -> isValid(item) && other.isValid(item);
}
default Validator<T> or(Validator<T> other) {
return item -> isValid(item) || other.isValid(item);
}
default Validator<T> 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<String, Integer> stringToInt = Integer::parseInt;
Converter<String, User> 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<User> hasName = u -> u.name != null && !u.name.isBlank();
Validator<User> isAdult = u -> u.age >= 18;
Validator<User> hasEmail = u -> u.email != null && u.email.contains("@");
Validator<User> isValidUser = hasName.and(isAdult).and(hasEmail);
List<User> 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<User> validUsers = users.stream()
.filter(isValidUser::isValid)
.collect(Collectors.toList());
System.out.println("Valid users: " + validUsers);
// Output: Valid users: [Alice (age 30, alice@example.com)]
}
}6. Functional Interface Reference Table
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.
6.1 Core Interfaces
| 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 |
6.2 Bi-Variant Interfaces
| 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 |
6.3 Primitive Predicate Specializations
| Interface | Abstract Method | Avoids Boxing |
|---|---|---|
IntPredicate |
boolean test(int) |
Predicate<Integer> |
LongPredicate |
boolean test(long) |
Predicate<Long> |
DoublePredicate |
boolean test(double) |
Predicate<Double> |
6.4 Primitive Function Specializations
| 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 |
6.5 Primitive Consumer and Supplier Specializations
| 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 |
6.6 Primitive Operator Specializations
| 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 |
7. Complete Practical Example: Validation Framework
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<String> errors;
private ValidationResult(boolean valid, List<String> 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<String> allErrors = new ArrayList<>(a.errors);
allErrors.addAll(b.errors);
return new ValidationResult(false, allErrors);
}
boolean isValid() { return valid; }
List<String> getErrors() { return errors; }
@Override
public String toString() {
return valid ? "VALID" : "INVALID: " + errors;
}
}
// --- Validator: wraps a Predicate with an error message ---
@FunctionalInterface
interface Validator<T> {
ValidationResult validate(T t);
default Validator<T> and(Validator<T> other) {
return t -> ValidationResult.combine(this.validate(t), other.validate(t));
}
// Factory method: create a Validator from a Predicate and error message
static <T> Validator<T> of(Predicate<T> 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<RegistrationForm> usernameNotEmpty = Validator.of(
f -> f.username != null && !f.username.isBlank(),
"Username is required"
);
Validator<RegistrationForm> usernameMinLength = Validator.of(
f -> f.username != null && f.username.length() >= 3,
"Username must be at least 3 characters"
);
Validator<RegistrationForm> usernameMaxLength = Validator.of(
f -> f.username == null || f.username.length() <= 20,
"Username must be at most 20 characters"
);
Validator<RegistrationForm> emailValid = Validator.of(
f -> f.email != null && f.email.matches("^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"),
"Email is invalid"
);
Validator<RegistrationForm> passwordStrong = Validator.of(
f -> f.password != null && f.password.length() >= 8,
"Password must be at least 8 characters"
);
Validator<RegistrationForm> passwordHasDigit = Validator.of(
f -> f.password != null && f.password.chars().anyMatch(Character::isDigit),
"Password must contain at least one digit"
);
Validator<RegistrationForm> passwordHasUpper = Validator.of(
f -> f.password != null && f.password.chars().anyMatch(Character::isUpperCase),
"Password must contain at least one uppercase letter"
);
Validator<RegistrationForm> ageValid = Validator.of(
f -> f.age >= 13 && f.age <= 120,
"Age must be between 13 and 120"
);
// --- Compose all validators into one ---
Validator<RegistrationForm> fullValidator = usernameNotEmpty
.and(usernameMinLength)
.and(usernameMaxLength)
.and(emailValid)
.and(passwordStrong)
.and(passwordHasDigit)
.and(passwordHasUpper)
.and(ageValid);
// --- Test data ---
List<RegistrationForm> 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<RegistrationForm> 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<RegistrationForm, String> 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 |
8. Summary
- A functional interface has exactly one abstract method. It is the target type for lambda expressions and method references.
- The
@FunctionalInterfaceannotation is optional but recommended — it prevents accidental breakage by enforcing the single-method rule at compile time. - Java had functional interfaces before Java 8 (
Runnable,Comparator,Callable). Java 8 formalized the concept and added thejava.util.functionpackage. - The four core interfaces are Predicate (test), Function (transform), Consumer (act), and Supplier (provide).
- UnaryOperator and BinaryOperator are specializations of Function and BiFunction for same-type operations.
- Bi-variants (BiFunction, BiPredicate, BiConsumer) handle two-parameter scenarios.
- Primitive specializations (IntPredicate, LongFunction, etc.) avoid autoboxing for better performance.
- Composition is the key power: chain predicates with
and()/or()/negate(), functions withandThen()/compose(), and consumers withandThen(). - Create custom functional interfaces when you need more than two parameters, checked exceptions, or domain-specific naming.