DevNotes
Support
Java 8 & Streams · Chapter 15

Functional Interfaces

The engine behind Streams — @FunctionalInterface, lambdas, Predicate, Function, Consumer, Supplier, UnaryOperator, BinaryOperator, composition and chaining.

◷ 8 min read§ 17 sections? 6 interview questions{ } 30 code examples

#Introduction

One of the most important Java 8 topics. If Streams are the car, Functional Interfaces are the engine.

Most developers memorize filter(), map() and forEach(), but very few understand why these methods accept lambdas. This chapter changes that. You'll master what a functional interface is, @FunctionalInterface, lambda expressions, Predicate, Function, Consumer, Supplier, UnaryOperator, BinaryOperator, primitive functional interfaces, function composition and predicate chaining.

#What is a Functional Interface?

A Functional Interface is an interface that has exactly one abstract method.

Java
@FunctionalInterface
interface Calculator {

    int calculate(int a, int b);
}

✅ Valid — only one abstract method.

This is not a functional interface:

Java
interface Calculator {

    int add(int a, int b);

    int subtract(int a, int b);
}

❌ Because there are two abstract methods. (With @FunctionalInterface on it, the compiler reports an error.)

Why Did Java Introduce Functional Interfaces?

Before Java 8:

Java
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

Java 8:

Java
Runnable r = () -> System.out.println("Hello");

Cleaner. Shorter. Readable.

#What is a Lambda?

A lambda is simply an implementation of a functional interface.

Java
Calculator add = (a, b) -> a + b;

This is equivalent to:

Java
Calculator add = new Calculator() {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
};

The compiler creates the implementation for you.

#The Six You Must Know

Instead of creating your own, Java provides common ones in java.util.function:

InterfacePurposeMethod
Predicate<T>Test somethingboolean test(T t)
Function<T, R>Transform somethingR apply(T t)
Consumer<T>Consume / use somethingvoid accept(T t)
Supplier<T>Supply / create somethingT get()
UnaryOperator<T>Same type → same typeT apply(T t)
BinaryOperator<T>Combine two values of the same typeT apply(T a, T b)

If you master these six, Streams become much easier.

#Predicate

Meaning: ask a YES/NO question.

Java
boolean test(T t);
Java
Predicate<Integer> isEven = n -> n % 2 == 0;

System.out.println(isEven.test(10));
Output
true

Stream Connection

Java
numbers.stream()
       .filter(n -> n % 2 == 0)

What does filter() expect? A Predicate<Integer>. Internally:

Java
filter(Predicate<? super T> predicate)

One of the most common interview questions.

Predicate Chaining

Need adult (age ≥ 18) AND salary > 50,000:

Java
Predicate<Employee> adult = e -> e.getAge() >= 18;

Predicate<Employee> rich = e -> e.getSalary() > 50000;

Predicate<Employee> result = adult.and(rich);

Other operations:

Java
adult.or(rich);
adult.negate();

#Function<T, R>

Meaning: transform one object into another.

Java
R apply(T t);
Java
Function<String, Integer> length = String::length;

System.out.println(length.apply("Java"));
Output
4

Stream Connection

Java
employees.stream()
         .map(Employee::getName)

What does map() accept? A Function<Employee, String>. Internally:

Java
map(Function<? super T, ? extends R> mapper)

Function Composition

Need name → uppercase → length.

Java
Function<String, String> upper = String::toUpperCase;      // step 1
Function<String, Integer> length = String::length;         // step 2

Function<String, Integer> result = upper.andThen(length);  // combine

System.out.println(result.apply("java"));
java
JAVA
4
upper.andThen(length)

compose()

Reverse order. f.compose(g) means run g first, then f:

input
g(input)
f(g(input))
f.compose(g) — interview favourite

#Consumer

Meaning: consume a value, return nothing.

Java
void accept(T t);
Java
Consumer<String> print = System.out::println;

print.accept("Hello");
Output
Hello

Stream Connection

Java
stream.forEach(System.out::println);

What does forEach() accept? A Consumer<T>.

#Supplier

Meaning: provide a value. No input, one output.

Java
T get();
Java
Supplier<UUID> supplier = UUID::randomUUID;

System.out.println(supplier.get());

Every call creates a new UUID.

Stream Connection

Java
Stream.generate(UUID::randomUUID)

generate() accepts a Supplier.

#UnaryOperator

A special type of Function: input and output are the same type.

Java
UnaryOperator<String> upper = String::toUpperCase;

System.out.println(upper.apply("java"));
Output
JAVA

Equivalent to Function<String, String>, but more readable.

#BinaryOperator

Two inputs, one output — all the same type.

Java
BinaryOperator<Integer> add = Integer::sum;

System.out.println(add.apply(10, 20));
Output
30

Stream Connection

Java
reduce(Integer::sum)

reduce() accepts a BinaryOperator.

#Primitive Functional Interfaces

Avoid boxing.

Instead ofUse
Function<Integer, Integer>IntUnaryOperator
Predicate<Integer>IntPredicate
Java
IntPredicate even = n -> n % 2 == 0;

IntUnaryOperator square = n -> n * n;

These are faster because they avoid boxing.

#Real Spring Boot Examples

Java
// Filter active users
Predicate<User> active = User::isActive;

users.stream()
     .filter(active)
     .toList();

// Convert entity to DTO
Function<User, UserDTO> mapper = UserMapper::toDto;

users.stream()
     .map(mapper)
     .toList();

// Logging
Consumer<Order> logger = System.out::println;
orders.forEach(logger);

// Generate token
Supplier<String> token = () -> UUID.randomUUID().toString();

#How Streams Use Functional Interfaces

Stream methodFunctional interface
filter()Predicate
map()Function
forEach()Consumer
generate()Supplier
reduce()BinaryOperator
mapToInt()ToIntFunction
sorted()Comparator (a functional interface)

Memorize this table. It's a very common interview question.

#Interview Questions

Q1What is a Functional Interface?

An interface with exactly one abstract method.

Q2Can a Functional Interface have default methods?

Yes:

Java
@FunctionalInterface
interface Demo {

    void run();

    default void print() {
        System.out.println("Default");
    }
}

Still valid.

Q3Can it have static methods?

Yes. Only abstract methods are counted. (Abstract methods that override public Object methods, like equals(), aren't counted either — that's why Comparator is functional.)

Q4Difference between Function and Consumer?
FunctionConsumer
Returns a valueReturns nothing
apply()accept()
Q5Difference between Predicate and Function?
PredicateFunction
Returns booleanReturns any type
Used in filter()Used in map()
Q6Difference between Supplier and Consumer?
SupplierConsumer
Produces dataUses data
No inputTakes input

#Practice Problems

Predicate

  1. Check if a number is even.
  2. Check if a string is empty.
  3. Check if an employee's salary is greater than 50,000.
  4. Combine two predicates using and().
  5. Use negate() to find odd numbers.

Function

  1. Convert a string to uppercase.
  2. Find the length of a string.
  3. Convert Employee to employee name.
  4. Convert Employee to salary.
  5. Chain two functions using andThen().

Consumer

  1. Print employee names.
  2. Print salaries.
  3. Print uppercase names.
  4. Log employee details.
  5. Use forEach() with a consumer.

Supplier

  1. Generate a UUID.
  2. Generate a random integer.
  3. Generate the current timestamp.
  4. Return "Guest" as a default user.
  5. Create a new Employee object.

5+ Years Interview Challenge

Implement an employee processing pipeline using reusable functional interfaces.

Java
Predicate<Employee> highSalary = e -> e.getSalary() > 60_000;

Function<Employee, String> toName = Employee::getName;

Consumer<String> print = System.out::println;

employees.stream()
         .filter(highSalary)
         .map(toName)
         .sorted()
         .forEach(print);

Why is this good?

  • Business rules (Predicate) are reusable.
  • Mapping logic (Function) is reusable.
  • Output logic (Consumer) is reusable.
  • The stream pipeline reads like a sentence.

A pattern you'll often see in well-designed enterprise applications.

#Senior Java Tips

1. Think in terms of intent

Instead of remembering interface names, remember their purpose:

InterfaceQuestion it answers
Predicate"Should I keep this?"
Function"How should I transform this?"
Consumer"What should I do with this?"
Supplier"How do I create this?"

2. Prefer method references when they improve readability

Instead of name -> name.toUpperCase(), use String::toUpperCase. But don't force method references if they make the code harder to understand.

3. Don't create custom functional interfaces unnecessarily

Before writing your own interface, check whether one from java.util.function already fits your use case. Standard interfaces make your code easier for other Java developers to understand.

#Chapter Summary

  • A functional interface has exactly one abstract method
  • A lambda implements a functional interface
  • Predicate, Function, Consumer, Supplier, UnaryOperator, BinaryOperator
  • and() / or() / negate() and andThen() / compose()
  • Primitive variants (IntPredicate, IntUnaryOperator, …)
  • Which Stream method takes which interface