DEV Community

Priyank Bhardwaj
Priyank Bhardwaj

Posted on

Functional Interfaces You Must Know - The Backbone of Java Streams

Introduction

In the last part, we learned what Streams are and how they simplify working with data in Java. But behind every powerful stream operation like filter(), map(), or forEach(), there’s something magical happening — Functional Interfaces.

Think of them as the heart and soul of Java 8’s functional programming.
Without them, Streams wouldn’t even exist.

Let’s understand these step-by-step — in a simple way.


What Is a Functional Interface?

A Functional Interface is an interface that has exactly one abstract method. This is what allows us to use lambda expressions.

@FunctionalInterface
interface MyPrinter {
    void print(String message);
}

public class Example {
    public static void main(String[] args) {
        // Using lambda
        MyPrinter printer = msg -> System.out.println(msg);
        printer.print("Hello Functional Interfaces!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Key rule: One abstract method = Functional Interface
Annotation: @FunctionalInterface (optional but recommended)

It is highly recommended that the annotation @FunctionalInterface is used to create functional interfaces as:

  • If you try to add another abstract method it will lead to a compilation error.
  • The whole reason lambdas work is because the compiler can map the lambda to that single abstract method.
  • Once there’s ambiguity (more than one abstract method), Java can’t infer what the lambda should implement — so it disallows it.

Note: You can still add default and static methods to your functional interfaces, only condition is that it should have exactly 1 abstract method.

Why Are They So Important?

Functional interfaces make it possible to pass behavior as data.
That’s why you can do things like:

list.stream()
    .filter(name -> name.startsWith("A"))   // Predicate
    .map(String::toUpperCase)               // Function
    .forEach(System.out::println);          // Consumer
Enter fullscreen mode Exit fullscreen mode

Each of those methods (filter, map, forEach) expects a functional interface.

There are some core functional interfaces that are used in streams: Predicate, Function, Consumer , Supplier, Bifunction, UnaryOperator and BinaryOperator.

We will try and understand each of them now.


Predicate

Represents a boolean condition on a value.

Method: boolean test(T t)

Here T is a generic type parameter. It represents the type of the input value that the predicate will test.

Example

Predicate<Integer> isEven = n -> n % 2 == 0;

System.out.println(isEven.test(4));  // true
System.out.println(isEven.test(7));  // false
Enter fullscreen mode Exit fullscreen mode

Used in Streams:
filter() uses a Predicate to decide which elements to keep.

List<Integer> evens = numbers.stream()
                             .filter(n -> n % 2 == 0)
                             .toList();
Enter fullscreen mode Exit fullscreen mode

Function

Takes one value and returns another.

Method: R apply(T t)

Here T,R are generic type parameter. T represents the type of the input value that the Function will apply and R represents the output's return type.

Example

Function<String, Integer> lengthFinder = str -> str.length();
System.out.println(lengthFinder.apply("Java"));  // 4
Enter fullscreen mode Exit fullscreen mode

map() uses a Function to transform data.

List<Integer> lengths = words.stream()
                             .map(String::length)
                             .toList();
Enter fullscreen mode Exit fullscreen mode

Consumer

Takes a value and performs an action, but returns nothing.

Method: void accept(T t)

Example

Consumer<String> printer = msg -> System.out.println("Message: " + msg);
printer.accept("Hello, World!");
Enter fullscreen mode Exit fullscreen mode

forEach() uses a Consumer to process elements.

names.stream()
     .forEach(name -> System.out.println("Hello, " + name));
Enter fullscreen mode Exit fullscreen mode

Supplier

Supplies a value — takes nothing, just gives something back.

Method: T get()

Example:

Supplier<Double> randomValue = () -> Math.random();
System.out.println(randomValue.get());
Enter fullscreen mode Exit fullscreen mode

Used when:
You want to provide values lazily (e.g., in factory methods or testing).


BiFunction

Takes two inputs, returns one output.

Method: R apply(T t, U u)

Here T,R,U are generic type parameter. T,U in BiFunction represents the type of the input value that the Function will apply and R represents the output's return type.

BiFunction<Integer, Integer, Integer> adder = (a, b) -> a + b;
System.out.println(adder.apply(5, 7)); // 12
Enter fullscreen mode Exit fullscreen mode

UnaryOperator

A Function where input and output types are same.

UnaryOperator<Integer> square = n -> n * n;
System.out.println(square.apply(4)); // 16
Enter fullscreen mode Exit fullscreen mode

BinaryOperator

A BiFunction where all types are same.

BinaryOperator<Integer> multiply = (a, b) -> a * b;
System.out.println(multiply.apply(3, 4)); // 12
Enter fullscreen mode Exit fullscreen mode

Relationship in the functional interface hierarchy

Interface Extends Method Use Case
Function<T, R> R apply(T t) Converts T → R
UnaryOperator<T> Function<T, T> T apply(T t) Transforms an element (T → T)
BiFunction<T, U, R> R apply(T t, U u) Combines two different types (T, U → R)
BinaryOperator<T> BiFunction<T, T, T> T apply(T t1, T t2) Combines two values of same type (T, T → T)

Why functional interface are the backbone of java streams?

Every Stream operation depends on these interfaces:

Stream Method Functional Interface Purpose
filter() Predicate<T> Keep elements matching a condition
map() Function<T, R> Transform elements
forEach() Consumer<T> Act on each element
reduce() BinaryOperator<T> Combine elements
sorted() Comparator<T> Compare elements

The table shows which stream method uses which functional interface to serve what purpose.


Conclusion

Functional Interfaces are like the grammar of Java 8 Streams.
Once you know their roles, you can easily read and write stream pipelines fluently — just like sentences in Java’s new functional language!

🔗 Next Up (Part 3): Creating Streams

Top comments (0)