Functional Interfaces

The built-in functional interfaces in java.util.function — Function, Predicate, Consumer, Supplier, and how to compose them.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: A functional interface has exactly one abstract method. It can be implemented with a lambda expression. Java 8 added java.util.function with 40+ built-in functional interfaces covering all common patterns: Function (transform), Predicate (test), Consumer (consume), Supplier (produce), and BiXxx variants for two inputs.

🔍 What Is It?

Core Functional Interfaces:

Function<T, R> — takes T, returns R

  • →apply(T t): R
  • →andThen(Function): compose after
  • →compose(Function): compose before

Predicate<T> — takes T, returns boolean

  • →test(T t): boolean
  • →and(Predicate), or(Predicate), negate()

Consumer<T> — takes T, returns void

  • →accept(T t)
  • →andThen(Consumer)

Supplier<T> — takes nothing, returns T

  • →get(): T

BiFunction<T, U, R> — takes T and U, returns R

BiPredicate<T, U> — takes T and U, returns boolean

BiConsumer<T, U> — takes T and U, returns void

Primitive Specializations (avoid boxing overhead):

  • →IntFunction<R>, LongFunction<R>, DoubleFunction<R>
  • →IntPredicate, LongPredicate, DoublePredicate
  • →IntConsumer, IntSupplier, IntUnaryOperator
  • →ToIntFunction<T>, ToLongFunction<T>

UnaryOperator<T> — Function<T, T> (same type in and out)

BinaryOperator<T> — BiFunction<T, T, T>

🎯 Why Do We Need It?

Functional interfaces are the types that lambdas implement. Without them, there would be no standard way to pass functions as arguments. The java.util.function package provides standard types that the entire Java ecosystem uses — Streams, Optional, CompletableFuture, and all major frameworks speak this language.

🏗️ What Happens in Real Projects?

Functional interfaces appear everywhere:

// Spring Data Specification (Predicate)
Specification<User> isActive = (root, query, cb) -> cb.isTrue(root.get("active"));

// Optional (Supplier, Consumer, Function)
Optional.ofNullable(value)
    .map(String::toUpperCase)          // Function
    .filter(s -> s.length() > 3)       // Predicate
    .ifPresent(System.out::println);   // Consumer

// CompletableFuture (Function, Consumer, Supplier)
CompletableFuture.supplyAsync(() -> fetchData())  // Supplier
    .thenApply(data -> transform(data))            // Function
    .thenAccept(result -> save(result));            // Consumer

⚙️ How It Works

@FunctionalInterface annotation:

Optional but recommended. Causes a compile error if the interface has more than one abstract method. Documents intent.

Function Composition:

Functions can be composed into pipelines:

Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, Integer> length = String::length;

Function<String, Integer> pipeline = trim.andThen(upper).andThen(length);
int result = pipeline.apply("  hello  ");  // 5

Predicate Composition:

Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> notTooLong = s -> s.length() <= 100;
Predicate<String> valid = notEmpty.and(notTooLong);

💻 Example

Functional Interfaces in Enterprise Code — How to use and compose functional interfaces for clean, reusable business logic.

java
// Function<T, R> — transformation
Function<String, Integer> strLength = String::length;
Function<Integer, String> intToStr = n -> "Value: " + n;

// Compose: first strLength, then intToStr
Function<String, String> composed = strLength.andThen(intToStr);
System.out.println(composed.apply("hello"));  // "Value: 5"

// Predicate<T> — boolean test
Predicate<String> isEmail = s -> s.contains("@") && s.contains(".");
Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> validEmail = notBlank.and(isEmail);
Predicate<String> invalidEmail = validEmail.negate();

System.out.println(validEmail.test("user@example.com"));  // true
System.out.println(validEmail.test("notanemail"));         // false

// Consumer<T> — side effect
Consumer<String> log = msg -> System.out.println("[LOG] " + msg);
Consumer<String> audit = msg -> auditService.record(msg);
Consumer<String> logAndAudit = log.andThen(audit);
logAndAudit.accept("User login");

// Supplier<T> — lazy value production
Supplier<Connection> dbConn = () -> dataSource.getConnection();
// Connection not created until dbConn.get() is called

// UnaryOperator<T> — transform same type
UnaryOperator<String> normalize = s -> s.trim().toLowerCase();
UnaryOperator<BigDecimal> roundCurrency = 
    n -> n.setScale(2, RoundingMode.HALF_UP);

// BinaryOperator<T> — combine two of same type
BinaryOperator<BigDecimal> sum = BigDecimal::add;
BinaryOperator<String> concat = String::concat;

🤔 Common Confusions

❓ What is the difference between Function and UnaryOperator?

UnaryOperator<T> extends Function<T, T> — it is a Function where the input and output type are the same. Use UnaryOperator when you are transforming a value to the same type (String -> String, Integer -> Integer). Use Function when input and output types differ (String -> Integer). The distinction helps readability and enables some additional composition methods.

❓ Why are there primitive specializations like IntFunction and LongPredicate?

Generic types like Function<Integer, R> require boxing (int -> Integer) and unboxing, which has overhead. Primitive specializations like IntFunction<R> work directly with int primitives, avoiding boxing. For performance-critical code processing millions of numbers, use primitive specializations. For typical business code, the generic versions are fine.

🚨 Common Production Issues

Not using primitive specializations for numeric streams — using Stream<Integer> instead of IntStream boxes every integer. For large numeric datasets, use IntStream, LongStream, DoubleStream.

Stateful lambdas in parallel streams — if a lambda captures mutable state, parallel stream operations are not safe. Keep lambdas stateless.

✅ Best Practices

Use built-in functional interfaces (Function, Predicate, Consumer, Supplier) instead of creating custom ones.

Create custom functional interfaces only for domain-specific semantics that add clarity.

Annotate custom functional interfaces with @FunctionalInterface.

Use primitive specializations (IntFunction, etc.) for performance-critical numeric code.

Name complex functions and predicates for readability: Predicate<User> isEligibleForPromotion = ...

🏛️ Architect's Note

Senior Architect's Perspective

Functional interfaces are the bridge between OOP and FP in Java. In domain-driven design, functional interfaces can represent domain operations: Validator<Order>, PricingStrategy, DiscountRule. These are more flexible than class hierarchies — you can compose them, swap them at runtime, and test them in isolation. This is the Strategy pattern expressed functionally.

💬 Frequently Asked Questions

Can a functional interface have default and static methods?

Yes. A functional interface can have any number of default and static methods — only abstract methods count toward the one-abstract-method limit. This is how Predicate has and(), or(), negate() as default methods while still being a functional interface with test() as the single abstract method.

🎯 Key Takeaways

1Functional interface = interface with exactly one abstract method
2Core interfaces: Function (transform), Predicate (test), Consumer (side effect), Supplier (produce)
3Compose functions with andThen/compose; predicates with and/or/negate
4Use primitive specializations (IntFunction, etc.) to avoid boxing overhead
5@FunctionalInterface annotation enforces the single-abstract-method constraint
6Built-in interfaces are used by Streams, Optional, CompletableFuture — learn them well

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages