Skip to content

Functional Interfaces — Predicate, Function, Consumer, Supplier, and Custom FIs

DodaTech Updated 2026-06-28 5 min read

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

  1. Using Function when Predicate is appropriate. Function<T, Boolean> works but Predicate<T> is more idiomatic for boolean tests.
  2. Using Supplier with already-computed values. Supplier is for deferred computation. Passing a pre-computed value defeats the purpose.
  3. Side effects in Function. Functions should be pure (no side effects). Use Consumer for side effects.
  4. Forgetting @FunctionalInterface. While optional, it catches accidental addition of multiple abstract methods.
  5. Confusing andThen and compose. 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

What is a functional interface?

An interface with exactly one abstract method. Lambda expressions can be used to create instances of functional interfaces. Examples: Runnable, Comparable, Predicate, Function.

Why does java.util.function have so many interfaces?

To support primitives (IntFunction, LongConsumer) and arity (BiFunction, BiPredicate). Without specialized interfaces, every use would require autoboxing.

What is `IntFunction` used for?

It maps an int primitive to a result object. Example: IntFunction<String[]> arrayCreator = String[]::new.

Can a functional interface extend another functional interface?

Yes, but only if it does not add any new abstract methods (it can add default or static methods).

What is `ObjIntConsumer`?

A BiConsumer variant where one argument is an object and the other is an int primitive. Example: (list, index) -> list.get(index).

Mini Project

Write a program FunctionalInterfacesDemo.java that:

  1. Creates a Predicate chain to validate user input (not null, length > 3, contains no spaces)
  2. Creates a Function pipeline: trim -> uppercase -> reverse
  3. Uses Consumer with andThen() to log and validate
  4. Uses Supplier for Lazy Loading of configuration (simulate with Thread.sleep)
  5. Creates a custom @FunctionalInterface Transformer<T> with T apply(T input) and default methods andThen, compose
  6. Implements a BiFunction<String, Integer, String> that repeats a string n times
  7. 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