Home/Learn/Java A–Z/Functional Interfaces

Functional Interfaces

Intermediate
Streams & Functional Java

Master the java.util.function package — Predicate, Function, Consumer, Supplier, and their Bi and primitive variants used throughout the Streams API.

Overview

A functional interface has exactly one abstract method (SAM). The java.util.function package ships 43 ready-made functional interfaces covering every combination of input/output. Knowing the four core types and their composition methods is essential for working with Streams, Optional, CompletableFuture, and any functional-style Java code.

The Four Core Types

Predicate<T> — T → boolean (test) Function<T,R> — T → R (apply) Consumer<T> — T → void (accept) Supplier<T> — () → T (get)

Each has a Bi variant for two inputs: BiPredicate<T,U>, BiFunction<T,U,R>, BiConsumer<T,U>. UnaryOperator<T> extends Function<T,T>. BinaryOperator<T> extends BiFunction<T,T,T>.

CoreFunctional.java
import java.util.function.*;
import java.util.List;

public class CoreFunctional {
    public static void main(String[] args) {
        // Predicate<T> — test a condition
        Predicate<String> isLong   = s -> s.length() > 5;
        Predicate<String> startsA  = s -> s.startsWith("A");
        Predicate<String> both     = isLong.and(startsA);
        Predicate<String> either   = isLong.or(startsA);
        Predicate<String> notLong  = isLong.negate();

        System.out.println(both.test("Avocado"));   // true
        System.out.println(both.test("Ant"));       // false

        // Function<T,R> — transform a value
        Function<String, Integer> length = String::length;
        Function<String, String>  upper  = String::toUpperCase;
        Function<String, String>  pipeline = upper.andThen(s -> s + "!");

        System.out.println(pipeline.apply("hello")); // HELLO!

        // compose: g.compose(f) = g(f(x)); andThen: f.andThen(g) = g(f(x))
        Function<Integer, Integer> times2  = x -> x * 2;
        Function<Integer, Integer> plus3   = x -> x + 3;
        System.out.println(times2.andThen(plus3).apply(5)); // 13 = (5*2)+3
        System.out.println(times2.compose(plus3).apply(5)); // 16 = (5+3)*2

        // Consumer<T> — side effect, returns void
        Consumer<String> logger  = s -> System.out.println("[LOG] " + s);
        Consumer<String> saver   = s -> System.out.println("[SAVE] " + s);
        Consumer<String> both2   = logger.andThen(saver);
        both2.accept("event");

        // Supplier<T> — produce a value
        Supplier<List<String>> listFactory = java.util.ArrayList::new;
        List<String> l = listFactory.get();
        l.add("item");
        System.out.println(l);
    }
}

Primitive Specialisations & Custom Interfaces

Boxing/unboxing overhead matters in tight loops. The JDK provides primitive-specialised interfaces to avoid it:

IntPredicate, LongPredicate, DoublePredicate IntFunction<R>, IntUnaryOperator, IntBinaryOperator ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> IntConsumer, LongConsumer, DoubleConsumer IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier

Always use these over Predicate<Integer> in performance-critical paths.

PrimitiveAndCustom.java
import java.util.function.*;

@FunctionalInterface
interface ThrowingSupplier<T> {
    T get() throws Exception;

    // Static factory to wrap checked exceptions
    static <T> Supplier<T> unchecked(ThrowingSupplier<T> s) {
        return () -> {
            try { return s.get(); }
            catch (Exception e) { throw new RuntimeException(e); }
        };
    }
}

public class PrimitiveAndCustom {
    public static void main(String[] args) {
        // Primitive specialisation — no boxing
        IntPredicate isEven      = n -> n % 2 == 0;
        IntUnaryOperator doubler  = n -> n * 2;
        IntBinaryOperator add    = Integer::sum;

        System.out.println(isEven.test(4));          // true
        System.out.println(doubler.applyAsInt(5));   // 10
        System.out.println(add.applyAsInt(3, 7));    // 10

        // ToIntFunction — object → primitive
        ToIntFunction<String> length = String::length;
        System.out.println(length.applyAsInt("hello")); // 5

        // Custom functional interface handling checked exceptions
        Supplier<String> reader = ThrowingSupplier.unchecked(
            () -> new java.io.BufferedReader(
                new java.io.StringReader("test")).readLine());
        System.out.println(reader.get()); // test
    }
}

Method References as Functional Interfaces

Any method reference (::) can be assigned to a matching functional interface. This is the bridge between OOP methods and functional programming. The four types of method references map to these functional interface shapes:

ClassName::staticMethod → Function/Consumer/Supplier instance::instanceMethod → Function/Consumer/Supplier ClassName::instanceMethod (unbound) → BiFunction where first arg is receiver ClassName::new (constructor) → Supplier/Function

MethodRefAsFunctional.java
import java.util.function.*;
import java.util.List;
import java.util.stream.Collectors;

public class MethodRefAsFunctional {
    static int doubleIt(int n) { return n * 2; }
    int triple(int n)          { return n * 3; }

    public static void main(String[] args) {
        MethodRefAsFunctional obj = new MethodRefAsFunctional();

        // Static method reference
        IntUnaryOperator d = MethodRefAsFunctional::doubleIt;
        System.out.println(d.applyAsInt(5)); // 10

        // Instance method reference (bound)
        IntUnaryOperator t = obj::triple;
        System.out.println(t.applyAsInt(5)); // 15

        // Unbound instance method reference — first arg is the receiver
        Function<String, String> upper = String::toUpperCase;
        BiFunction<String, String, Boolean> startsWith = String::startsWith;
        System.out.println(upper.apply("hello")); // HELLO
        System.out.println(startsWith.apply("hello", "he")); // true

        // Constructor reference
        Supplier<List<String>>     listNew  = java.util.ArrayList::new;
        Function<String, StringBuilder> sbNew = StringBuilder::new;
        System.out.println(sbNew.apply("Java").append(" rocks")); // Java rocks

        // Practical: collect using constructor reference
        List<String> words = List.of("one", "two", "three");
        List<String> upper2 = words.stream()
            .map(String::toUpperCase)   // unbound method ref
            .collect(Collectors.toList());
        System.out.println(upper2); // [ONE, TWO, THREE]
    }
}

Interactive Visualization

.source()
.filter()
.map()
.sorted()
.collect()
1
2
3
4
5
6
7
8
stream.filter(n → n%2==0).map(n → n*n).sorted().collect(toList())
Source: a stream of integers [1, 2, 3, 4, 5, 6, 7, 8].
1 / 5

Key Points to Remember

  • Four core types: Predicate (test), Function (transform), Consumer (side-effect), Supplier (produce)
  • Composition: Predicate.and/or/negate; Function.andThen/compose; Consumer.andThen
  • Use primitive specialisations (IntPredicate, ToIntFunction…) to avoid boxing overhead
  • BiXxx variants accept two inputs; UnaryOperator/BinaryOperator are same-type Function specialisations
  • @FunctionalInterface is optional but enables compile-time enforcement of the SAM constraint
  • Method references (::) are syntactic sugar for lambdas implementing matching functional interfaces

Practice Functional Interfaces in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the difference between Predicate, Function, Consumer, and Supplier?

MediumAmazon
2

What does Function.compose() do versus Function.andThen()?

MediumGoogle
3

What are primitive functional interfaces and why do they exist?

MediumOracle
4

How many abstract methods can a functional interface have?

EasyTCS
5

What is the difference between UnaryOperator and Function?

EasyMicrosoft

Ask Aria about Functional Interfaces

Your personal AI tutor — ask anything about this concept