Functional Interfaces — Predicate, Function, Consumer, Supplier, and Custom FIs
In this tutorial, you will learn about Functional Interfaces. We cover key concepts, practical examples, and best practices to help you master this topic.
Java's java.util.function package provides standard functional interfaces for lambda expressions, covering predicates, functions, consumers, and suppliers. These four categories — test, transform, consume, supply — cover the vast majority of functional patterns in everyday programming.
What You'll Learn
- The four core functional interfaces: Predicate, Function, Consumer, Supplier
- Specialized variants: BiFunction, BiPredicate, IntFunction, etc.
- Composing functional interfaces: andThen, compose, and, or, negate
- Creating custom functional interfaces
Why It Matters
Functional interfaces are the glue between lambdas and APIs. Understanding them lets you design APIs that accept behavior as parameters — the foundation of the Strategy pattern and Functional Programming in Java.
Real-World Use
Stream API uses all four categories. Predicate for filtering, Function for mapping, Consumer for forEach, Supplier for lazy initialization. Spring, JPA, and every modern framework uses them.
Predicate
Tests a condition — returns boolean:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Usage:
Predicate<String> isEmpty = String::isEmpty;
Predicate<Integer> isPositive = n -> n > 0;
Predicate<String> startsWithA = s -> s.startsWith("A");
// Composing
Predicate<String> startsWithAOrB = startsWithA.or(s -> s.startsWith("B"));
Predicate<String> notEmpty = isEmpty.negate();
Predicate<Integer> isPositiveAndEven = isPositive.and(n -> n % 2 == 0);
// Stream usage
list.stream().filter(isPositive.and(n -> n < 100));
Specialized Predicates
IntPredicate even = n -> n % 2 == 0; // avoids autoboxing
LongPredicate isLarge = n -> n > 1_000_000L;
DoublePredicate closeToZero = d -> Math.abs(d) < 0.0001;
BiPredicate<String, Integer> lengthCheck = (s, len) -> s.length() == len;
Function<T, R>
Transforms input to output:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Usage:
Function<String, Integer> lengthFunc = String::length;
Function<Integer, String> numberToString = Object::toString;
Function<String, String> upperCase = String::toUpperCase;
// Composing
Function<String, String> upperThenTrim = upperCase.andThen(String::trim);
Function<String, Integer> parseThenMul = ((Function<String, Integer>) Integer::parseInt)
.andThen(n -> n * 2);
// Identity
Function<String, String> identity = Function.identity();
Specialized Functions
// BiFunction — two inputs
BiFunction<String, String, String> concat = (a, b) -> a + " " + b;
// UnaryOperator — same input/output type
UnaryOperator<String> toUpper = String::toUpperCase;
// BinaryOperator — combine two inputs of same type
BinaryOperator<Integer> max = Integer::max;
// Primitive inputs
IntFunction<String> intToString = String::valueOf;
ToIntFunction<String> parseLength = String::length;
Consumer
Accepts input, returns nothing (side effect):
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Usage:
Consumer<String> print = System.out::println;
Consumer<String> log = s -> logger.info("Value: " + s);
// Chaining
Consumer<String> printAndLog = print.andThen(log);
list.forEach(printAndLog);
Specialized Consumers
BiConsumer<String, Integer> printEntry = (name, age) ->
System.out.println(name + ": " + age);
IntConsumer printInt = System.out::println;
DoubleConsumer printDouble = d -> System.out.printf("%.2f%n", d);
Supplier
Provides a value without input:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Usage:
Supplier<LocalDate> today = LocalDate::now;
Supplier<String> configValue = () -> System.getProperty("app.name", "default");
Supplier<List<String>> emptyList = ArrayList::new;
// Lazy evaluation
public void process(Supplier<Data> dataSupplier) {
// expensive computation only if needed
if (cache.isEmpty()) {
Data data = dataSupplier.get();
processData(data);
}
}
Specialized Suppliers
BooleanSupplier randomBool = () -> Math.random() > 0.5;
IntSupplier diceRoll = () -> ThreadLocalRandom.current().nextInt(1, 7);
DoubleSupplier pi = Math::random; // not really pi, but a double supplier
Custom Functional Interfaces
Create your own when the standard interfaces do not fit:
@FunctionalInterface
interface StringProcessor {
String process(String input);
}
StringProcessor trimProcessor = String::trim;
StringProcessor reverseProcessor = s -> new StringBuilder(s).reverse().toString();
StringProcessor pipeline = trimProcessor.andThen(reverseProcessor); // custom default method
With Default Methods
@FunctionalInterface
interface Logger {
void log(String message);
default void info(String message) {
log("[INFO] " + message);
}
default void error(String message) {
log("[ERROR] " + message);
}
static Logger consoleLogger() {
return System.out::println;
}
}
Common Mistakes
- Using
FunctionwhenPredicateis appropriate.Function<T, Boolean>works butPredicate<T>is more idiomatic for boolean tests. - Using
Supplierwith already-computed values.Supplieris for deferred computation. Passing a pre-computed value defeats the purpose. - Side effects in
Function. Functions should be pure (no side effects). UseConsumerfor side effects. - Forgetting
@FunctionalInterface. While optional, it catches accidental addition of multiple abstract methods. - Confusing
andThenandcompose.f.andThen(g)means apply f then g.f.compose(g)means apply g then f.
Practice Questions
1. What is the difference between Function<T, R> and UnaryOperator<T>?
UnaryOperator<T> extends Function<T, T> — both input and output are the same type. It is a specialization of Function.
2. What is the purpose of Consumer when you could just use a method?
Consumer allows behavior to be passed as a parameter. The forEach method accepts a Consumer to define what to do with each element.
3. How do you compose two Predicates?
Using predicate1.and(predicate2), predicate1.or(predicate2), or predicate.negate().
4. What is the difference between andThen and compose on Function?
f.andThen(g) applies f first, then g. f.compose(g) applies g first, then f — like mathematical composition.
5. Why is Supplier useful for lazy initialization?
The supplier is only invoked when get() is called. If the value is never needed, the computation never runs. This is useful for expensive or rarely-used resources.
Challenge Question:
Design a validation framework using functional interfaces. Create a Validator<T> functional interface with methods validate(T value) returning Optional<String> (error message). Provide static methods for common validators: notNull(), notEmpty(), matches(pattern), maxLength(int). Allow combining validators with and().
FAQ
Mini Project
Write a program FunctionalInterfacesDemo.java that:
- Creates a
Predicatechain to validate user input (not null, length > 3, contains no spaces) - Creates a
Functionpipeline: trim -> uppercase -> reverse - Uses
ConsumerwithandThen()to log and validate - Uses
Supplierfor Lazy Loading of configuration (simulate withThread.sleep) - Creates a custom
@FunctionalInterfaceTransformer<T>withT apply(T input)and default methodsandThen,compose - Implements a
BiFunction<String, Integer, String>that repeats a string n times - Uses primitive functional interfaces to avoid autoboxing
What's Next
Functional programming transforms how you Process data. But data must come from somewhere — files, databases, networks. Lesson 39 returns to I/O with File I/O Streams — FileInputStream, FileOutputStream, Buffered streams, and Data streams for binary I/O operations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro