JavaJava 8intermediate
Updated:

Functional Interfaces in Java: Predicate, Function, Consumer, Supplier

6 min read

The four core functional interfaces power every stream operation. Learn Predicate, Function, Consumer and Supplier with runnable examples and composition.

TL;DR – Quick Answer

A functional interface in Java is an interface with exactly one abstract method, which makes it usable as the target type for a lambda expression. The four core built-ins in java.util.function are Predicate<T> (T to boolean), Function<T,R> (T to R), Consumer<T> (T to void) and Supplier<T> (nothing to T) — they power filter, map, forEach and lazy creation respectively.

On This Page

Every lambda you write needs a type, and that type is always a functional interface. Master the four core ones — Predicate, Function, Consumer, Supplier — and stream signatures like filter(Predicate<? super T>) stop looking like noise and start telling you exactly what to pass. This page gives you the definition, the big four with runnable code, the composition methods interviewers probe, and the rules for writing your own.

What makes an interface "functional"

A functional interface has exactly one abstract method (often abbreviated SAM — Single Abstract Method). That single method is what a lambda expression implements.

Three things do not count against the limit:

  • default methods (they have bodies),
  • static methods (they have bodies),
  • abstract methods that match public methods of Object, like boolean equals(Object).

That last rule is why Comparator is functional even though it declares both compare and equals.

@FunctionalInterface
interface EmiCalculator {
    double emi(double principal, int months);   // the one abstract method

    default double totalPayable(double principal, int months) {
        return emi(principal, months) * months;
    }
}

public class SamDemo {
    public static void main(String[] args) {
        EmiCalculator flat = (p, m) -> (p + p * 0.10) / m;
        System.out.println(flat.emi(120_000, 12));          // 11000.0
        System.out.println(flat.totalPayable(120_000, 12)); // 132000.0
    }
}

The @FunctionalInterface annotation is optional but valuable: it makes the compiler reject any future second abstract method, protecting every lambda already written against the interface. This is abstraction at its purest — one behaviour, no ceremony.

The big four in java.util.function

Interface Abstract method Input → Output Powers
Predicate<T> boolean test(T t) T → boolean filter, removeIf
Function<T,R> R apply(T t) T → R map, computeIfAbsent
Consumer<T> void accept(T t) T → void forEach, peek
Supplier<T> T get() () → T orElseGet, lazy init

Memorise the mnemonic by role: Predicate tests, Function transforms, Consumer uses, Supplier provides.

Predicate: yes/no questions

import java.util.List;
import java.util.function.Predicate;

public class PredicateDemo {
    record Candidate(String name, int marks, boolean hasProject) {}

    public static void main(String[] args) {
        Predicate<Candidate> passed = c -> c.marks() >= 60;
        Predicate<Candidate> projectReady = Candidate::hasProject;

        // Compose: both conditions must hold
        Predicate<Candidate> interviewReady = passed.and(projectReady);

        List<Candidate> batch = List.of(
            new Candidate("Sneha", 72, true),
            new Candidate("Vikram", 55, true),
            new Candidate("Rahul", 81, false)
        );

        batch.stream()
             .filter(interviewReady)
             .forEach(c -> System.out.println(c.name())); // Sneha
    }
}

Notice and() — Predicate ships with and, or, negate and the static Predicate.not(). Composing small named predicates beats one giant boolean expression: each piece is testable and reusable.

Function: transformations

import java.util.function.Function;

public class FunctionDemo {
    public static void main(String[] args) {
        Function<String, String> trim = String::trim;
        Function<String, Integer> length = String::length;

        // andThen: trim first, then measure
        Function<String, Integer> trimmedLength = trim.andThen(length);

        // compose: measure the UNtrimmed string? No — compose runs the
        // argument first: length.compose(trim) == trim.andThen(length)
        System.out.println(trimmedLength.apply("  Hyderabad  ")); // 9
    }
}

f.andThen(g) runs f then g; f.compose(g) runs g then f. Interviewers ask for the difference constantly, and mixing them up in a pipeline produces wrong results, not errors — so anchor it now: andThen = left to right.

Consumer: side effects

Consumer<T> accepts a value and returns nothing — printing, saving, publishing. forEach takes one, and andThen chains two consumers to run in sequence over the same element:

Consumer<String> log = s -> System.out.println("LOG: " + s);
Consumer<String> audit = s -> System.out.println("AUDIT: " + s);
List.of("enrolled", "paid").forEach(log.andThen(audit));

Supplier: deferred creation

Supplier<T> takes nothing and produces a value only when get() is called. That deferral is the whole point:

Optional<Config> cached = Optional.empty();
Config cfg = cached.orElseGet(() -> loadFromDisk()); // runs only if empty

With orElse(loadFromDisk()), the load would execute even when the Optional has a value. The orElse vs orElseGet distinction is covered further in Java Optional.

Common mistake: A common mistake beginners make is treating Function<T, Boolean> and Predicate<T> as interchangeable. They are different types with different methods (apply returns a boxed Boolean, test returns primitive boolean), and filter accepts only Predicate. Choose Predicate whenever the answer is yes/no.

Beyond the big four: variants you will actually meet

  • Bi-versions: BiPredicate<T,U>, BiFunction<T,U,R>, BiConsumer<T,U> take two arguments. Map.forEach((key, value) -> ...) uses BiConsumer.
  • Operators: UnaryOperator<T> is a Function<T,T> (used by List.replaceAll); BinaryOperator<T> is a BiFunction<T,T,T> (used by reduce).
  • Primitive specializations: IntPredicate, IntFunction<R>, ToIntFunction<T>, IntSupplier and friends avoid boxing on hot paths. IntStream.map(IntUnaryOperator) exists precisely so a million ints are not wrapped into a million Integers.
  • Old friends that qualify: Runnable (() → void), Callable<V> (() → V, throws), Comparator<T> — all pre-Java-8 interfaces that happen to have one abstract method, so lambdas work with them everywhere.

Pro tip: Before defining a custom functional interface, scan java.util.function — with 43 interfaces, the shape you need almost always exists. A custom interface is justified mainly when you need a domain name for readability (like EmiCalculator above) or a checked exception in the signature.

How they plug into streams

Read a stream pipeline and you can now name every parameter type:

list.stream()
    .filter(c -> c.marks() >= 60)      // Predicate<Candidate>
    .map(Candidate::name)              // Function<Candidate,String>
    .sorted(String::compareTo)         // Comparator<String>
    .forEach(System.out::println);     // Consumer<String>

This is why we insist you learn functional interfaces before streams: once you can label each slot, stream documentation reads itself, and compiler errors about mismatched lambdas point straight at the fix.

Interview note: A standard rapid-fire round: "Name the functional interface for () → T. For T → boolean. For (T, U) → R." Practise answering with the interface and its method — "Supplier, get()", "Predicate, test()", "BiFunction, apply()". Following up abstract-method names separates candidates who used these from those who memorised a table.

Writing your own functional interface

Do it when the domain deserves a name or the built-ins cannot express the signature (checked exceptions, three-plus parameters):

@FunctionalInterface
interface RowMapper<R> {
    R map(java.sql.ResultSet rs, int rowNum) throws java.sql.SQLException;
}

This mirrors the design Spring uses for JDBC row mapping: two parameters plus a checked SQLException, which no java.util.function type allows. Keep custom interfaces generic where practical, annotate them, and give the method a verb name.

Two design guidelines when you publish methods that accept functional interfaces:

  • Widen with wildcards. Accept Predicate<? super T> rather than Predicate<T>, exactly as the JDK does in Stream.filter. A Predicate<Object> should be usable wherever a Predicate<String> is expected.
  • Avoid ambiguous overloads. If a class offers both run(Runnable r) and run(Callable<Integer> c), the call run(() -> 1) may force callers into ugly casts because the compiler cannot pick a target type. Give the overloads different names instead — this is why ExecutorService has execute and submit behaving differently, and it is a real API-design question at senior interviews.

Lambdas, anonymous classes and functional interfaces together

A functional interface does not require a lambda — all three of these produce a valid Predicate<String>:

// 1. Lambda
Predicate<String> p1 = s -> s.isEmpty();

// 2. Method reference
Predicate<String> p2 = String::isEmpty;

// 3. Anonymous class (pre-Java-8 style, still legal)
Predicate<String> p3 = new Predicate<String>() {
    @Override public boolean test(String s) { return s.isEmpty(); }
};

All three are interchangeable at the call site. Reach for the anonymous class only when you genuinely need instance state or a self-referencing this; otherwise the lambda or method reference is shorter and clearer. Understanding that the interface is the contract and the lambda is merely one way to satisfy it is the key conceptual step — it explains why decade-old APIs like Collections.sort accepted lambdas on day one of Java 8 without any change.

Where to go next

You now hold the vocabulary that the entire Java 8 ecosystem is written in. Next, put it to work in Java Streams, where all four interfaces appear in every pipeline, then review the broader Java 8 features guide to see how the pieces connect. In our Java Full Stack course, this topic is a single evening of drills — big four, composition methods, one custom interface — and it pays back in every stream exercise that follows.

Frequently Asked Questions

What is a functional interface in Java?
It is an interface that declares exactly one abstract method, such as Runnable (run) or Comparator (compare). Because there is only one method to implement, a lambda expression or method reference can stand in for an instance of it. Default and static methods do not count toward the one-method limit.
Is the @FunctionalInterface annotation mandatory?
No. Any interface with a single abstract method is functional whether annotated or not. The annotation is a compile-time guard: if someone later adds a second abstract method, compilation fails immediately instead of silently breaking every lambda that used the interface. Always add it to interfaces you design for lambdas.
What is the difference between Predicate and Function in Java?
Predicate<T> takes a T and always returns a primitive boolean — it answers yes/no questions and drives filter(). Function<T,R> takes a T and returns any type R — it transforms values and drives map(). A Function<T,Boolean> compiles but is not a Predicate; they are unrelated types with different methods (test vs apply).
When would I use Supplier instead of passing a value directly?
Use Supplier when the value is expensive to create and might not be needed, because the Supplier's get() runs only if called. Classic examples are Optional.orElseGet(() -> loadDefault()) and lazy logging. Passing the value directly would compute it every time, needed or not.
What are BiFunction and BiPredicate?
They are two-argument versions of Function and Predicate: BiFunction<T,U,R> takes two inputs and returns a result, and BiPredicate<T,U> takes two inputs and returns boolean. There is no TriFunction in the JDK; beyond two arguments you define your own functional interface or group parameters into a class.
Can a functional interface have default and static methods?
Yes, any number of them. Only abstract methods are limited to one. Predicate itself proves this: it has one abstract method test(), plus default methods and(), or() and negate(), plus static methods isEqual() and not(). Methods inherited from Object, like equals(), also do not count.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 14 July 2026 LinkedIn
Chat with us