跪拜 Guibai
← Back to the summary

The Four Java Functional Interfaces That Power Every Stream Pipeline

1. Foreword

Today, Java has iterated to version 26, bringing many modern new features such as virtual threads, pattern matching, Records, and sealed classes. However, Java 8 is still the most core and classic milestone version of the Java ecosystem, bar none. And the most disruptive core innovation of Java 8, which still runs through all projects and is frequently used in daily development, is undoubtedly the functional programming system.

The java.util.function core package added in JDK 8 is the cornerstone of the entire Java functional programming system, supporting core features like Lambda expressions, Stream operations, and method references. Even in higher-version Java projects, high-frequency scenarios such as Stream collection processing, behavior parameterization programming, and Optional null handling still completely rely on this set of functional interfaces.

Many developers use methods like Stream.filter(), map(), and forEach() to write business code year-round, but most only apply templates. They have a superficial understanding of the underlying functional interface principles, combination rules, and performance trade-offs. They cannot flexibly expand when encountering complex business scenarios and often lose points on this knowledge in interviews.

This article will explain from scratch, classify and sort out, analyze source code, implement in practice, and summarize pitfalls, covering all commonly used basic interfaces under java.util.function. Whether you are using Java 8, Java 17, or the latest Java 26, this knowledge set is completely universal. After reading, you can thoroughly master it, adapting to daily business development, interview preparation, and code architecture optimization.

2. Core Basic Concepts: What is a Functional Interface?

2.1 Definition Specification

Functional Interface: An interface with one and only one abstract method. It is allowed to have multiple default methods and static methods. Overriding methods from the Object class does not count.

Exclusive annotation: @FunctionalInterface

Function: Compile-time verification that the interface conforms to the functional interface specification, avoiding manual writing errors.

2.2 Core Value

2.3 Overall Interface Classification

All interfaces under the java.util.function package can be divided into 5 major categories, covering all daily development scenarios:

  1. Four Core Basic Interfaces: Single-parameter general-purpose interfaces (most commonly used in development)

  2. Binary Parameter Interfaces: Dual-parameter processing scenarios

  3. Primitive Type Specialization Interfaces: Avoid auto-boxing and unboxing, improving performance

  4. Unary/Binary Operator Interfaces: Operation scenarios where input and output types are consistent

  5. No-parameter/Null-value Interfaces: Adaptation for special scenarios

3. Four Core Basic Interfaces (Top Priority)

The four core interfaces are the cornerstone of the entire function package. All derived interfaces are extended based on them. Mastering these four means mastering 80% of functional programming scenarios.

3.1 Consumer Consumer Interface

Core Feature: Has input parameter, no return value, purely consumes data and executes operations

Abstract Method: void accept(T t)

Default Method: andThen() chained consumption

Applicable Scenarios: Traversal printing, data modification, log output, callback execution

import java.util.function.Consumer;

public class ConsumerDemo {
    public static void main(String[] args) {
        // Basic usage: consume a string
        Consumer<String> printStr = s -> System.out.println("Original content: " + s);
        printStr.accept("Java8 functional interface");

        // andThen chain combination: print original content first, then print uppercase content
        Consumer<String> upperPrint = s -> System.out.println("Uppercase content: " + s.toUpperCase());
        printStr.andThen(upperPrint).accept("function demo");
    }
}

High-frequency usage scenario: Stream.forEach(), collection traversal consumption

3.2 Supplier Supplier Interface

Core Feature: No input parameter, has return value, specifically produces/obtains data

Abstract Method: T get()

Applicable Scenarios: Object creation, random number generation, default value retrieval, data supply

import java.util.Random;
import java.util.function.Supplier;

public class SupplierDemo {
    public static void main(String[] args) {
        // Generate a random integer within 100
        Supplier<Integer> randomInt = () -> new Random().nextInt(100);
        System.out.println("Random number: " + randomInt.get());

        // Create a String object using a no-arg constructor (method reference simplification)
        Supplier<String> strSupplier = String::new;
        System.out.println("Empty string: " + strSupplier.get());
    }
}

High-frequency usage scenario: Optional.orElseGet(), lazy loading data

3.3 Function<T,R> Function Interface

Core Feature: Has input parameter T, has return value R, implements data conversion, type mapping

Abstract Method: R apply(T t)

Default Methods:

Applicable Scenarios: Type conversion, data processing, field mapping, business processing

import java.util.function.Function;

public class FunctionDemo {
    public static void main(String[] args) {
        // Basic usage: string to integer
        Function<String, Integer> strToInt = Integer::parseInt;
        System.out.println("Conversion result: " + strToInt.apply("666"));

        // Chain combination: compose multiplies first, then adds
        Function<Integer, Integer> add = x -> x + 2;
        Function<Integer, Integer> mul = x -> x * 3;
        System.out.println("compose result: " + add.compose(mul).apply(2)); // 2*3+2=8

        // Chain combination: andThen adds first, then multiplies
        System.out.println("andThen result: " + add.andThen(mul).apply(2)); // (2+2)*3=12
    }
}

High-frequency usage scenario: Stream.map() data mapping conversion

3.4 Predicate Predicate Interface

Core Feature: Has input parameter, returns boolean value, used for condition judgment, data filtering

Abstract Method: boolean test(T t)

Default Methods: and(), or(), negate() implement multi-condition combination

Applicable Scenarios: Data filtering, parameter validation, condition matching

import java.util.function.Predicate;

public class PredicateDemo {
    public static void main(String[] args) {
        // Basic judgment: is it greater than 10
        Predicate<Integer> gt10 = x -> x > 10;
        System.out.println(gt10.test(15)); // true

        // Multi-condition combination: 10 < x < 20
        Predicate<Integer> lt20 = x -> x < 20;
        boolean andResult = gt10.and(lt20).test(16);
        System.out.println("Interval judgment: " + andResult); // true

        // Negation
        boolean negateResult = gt10.negate().test(5);
        System.out.println("Negation result: " + negateResult); // true
    }
}

High-frequency usage scenario: Stream.filter() data filtering

4. Binary Parameter Derived Interfaces (Dual-parameter Scenarios)

The four core interfaces are all single-parameter. JDK 8 provides the BiXXX series of interfaces to adapt to dual-parameter business scenarios. Usage is completely consistent with single-parameter interfaces.

4.1 BiConsumer<T,U> Dual-parameter Consumer

Method: void accept(T t, U u), dual-parameter with no return, commonly used for key-value pair consumption

import java.util.function.BiConsumer;

public class BiConsumerDemo {
    public static void main(String[] args) {
        BiConsumer<String, Integer> userInfo = (name, age) -> 
            System.out.println("Name: " + name + ", Age: " + age);
        userInfo.accept("Zhang San", 22);
    }
}

High-frequency scenario: Map.forEach((k,v)->{})

4.2 BiFunction<T,U,R> Dual-parameter Mapping

Method: R apply(T t, U u), dual-parameter input, returns any type

import java.util.function.BiFunction;

public class BiFunctionDemo {
    public static void main(String[] args) {
        // Sum of two numbers
        BiFunction<Integer, Integer, Integer> sum = Integer::sum;
        System.out.println("Sum result: " + sum.apply(10, 20));
    }
}

4.3 BiPredicate<T,U> Dual-parameter Predicate

Method: boolean test(T t, U u), dual-parameter condition judgment

import java.util.function.BiPredicate;

public class BiPredicateDemo {
    public static void main(String[] args) {
        // Determine if string length is greater than a specified value
        BiPredicate<String, Integer> lenCheck = (str, len) -> str.length() > len;
        System.out.println(lenCheck.test("Java8 functional programming", 5)); // true
    }
}

5. Primitive Type Specialization Interfaces (Essential for Performance Optimization)

Generic interfaces only support reference types. Using primitive types will produce auto-boxing and unboxing, causing performance loss in high-frequency scenarios. JDK 8 provides primitive type specialization interfaces that directly operate on primitive types (int/long/double), with no boxing overhead and better performance.

5.1 Specialization Interface Classification

5.2 Practical Example

import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import java.util.function.ToIntFunction;

public class PrimitiveFuncDemo {
    public static void main(String[] args) {
        // IntConsumer: consume int type
        IntConsumer doubleNum = i -> System.out.println(i * 2);
        doubleNum.accept(10);

        // IntPredicate: judge int condition
        IntPredicate isEven = x -> x % 2 == 0;
        System.out.println("Is even: " + isEven.test(8));

        // ToIntFunction: convert reference type to int
        ToIntFunction<String> strLen = String::length;
        System.out.println("String length: " + strLen.applyAsInt("function"));
    }
}

Development Suggestion: For high-frequency loops and large-volume Stream processing, prioritize using primitive specialization interfaces to avoid boxing performance issues.

6. Operator-specific Interfaces (Input and Output Same Type)

For operation scenarios where input and output types are consistent, JDK provides two dedicated interfaces, which are subclasses of Function and BiFunction, simplifying mathematical operations and data aggregation logic.

6.1 UnaryOperator Unary Operation

Inherits Function<T,T>, single-parameter input, same-type return

import java.util.function.UnaryOperator;

public class UnaryOperatorDemo {
    public static void main(String[] args) {
        // Square operation
        UnaryOperator<Integer> square = x -> x * x;
        System.out.println("Square result: " + square.apply(6)); // 36
    }
}

6.2 BinaryOperator Binary Operation

Inherits BiFunction<T,T,T>, dual same-type parameters, same-type return, built-in max/min utility methods

import java.util.function.BinaryOperator;

public class BinaryOperatorDemo {
    public static void main(String[] args) {
        // Get maximum value
        BinaryOperator<Integer> maxFunc = BinaryOperator.maxBy(Integer::compare);
        System.out.println("Maximum value: " + maxFunc.apply(15, 28));

        // Get minimum value
        BinaryOperator<Integer> minFunc = BinaryOperator.minBy(Integer::compare);
        System.out.println("Minimum value: " + minFunc.apply(15, 28));
    }
}

High-frequency scenario: Stream.reduce() aggregation calculation, sum, max/min value

7. Comprehensive Practice: Stream Integrates All Core Interfaces

Through a complete case, connect the three core interfaces of Predicate, Function, and Consumer to simulate the full process of business data filtering, conversion, and traversal.

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

public class StreamFuncAllDemo {
    public static void main(String[] args) {
        List<String> dataList = Arrays.asList("5", "12", "8", "20", "3");

        // 1. Predicate interface: filter strings with length greater than 1
        Predicate<String> filterRule = s -> s.length() > 1;
        // 2. Function interface: convert string to integer
        Function<String, Integer> convertRule = Integer::parseInt;
        // 3. Consumer interface: traverse and output results
        Consumer<Integer> printRule = num -> System.out.println("Filter and convert result: " + num);

        // Stream chained processing
        dataList.stream()
                .filter(filterRule)
                .map(convertRule)
                .forEach(printRule);
    }
}

Output result:

Filter and convert result: 12
Filter and convert result: 20

8. Core Interface Quick Reference Table (Interview/Development Quick Reference)

Interface Name Parameter Count Return Value Core Purpose High-frequency Scenario
Consumer 1 void Consume data, execute operations forEach traversal
Supplier 0 T Produce data, provide default values orElseGet default value
Function 1 R Data conversion, type mapping Stream.map mapping
Predicate 1 boolean Condition judgment, data filtering Stream.filter filtering
BiXXX Series 2 Corresponding type Dual-parameter processing logic Map traversal, dual-parameter operations
UnaryOperator 1 Same as input type Single-parameter same-type operation Data self-operation
BinaryOperator 2 Same as input type Dual-parameter same-type operation reduce aggregation evaluation

9. Core Usage Summary of Default Methods

  1. Predicate: and() AND, or() OR, negate() NOT, implement multi-condition superposition

  2. Function: compose() pre-execution, andThen() post-execution, implement logic chain combination

  3. Consumer/BiConsumer: andThen() sequentially execute multiple consumption logics

  4. BinaryOperator: maxBy(), minBy() quickly obtain max/min values

10. Key Pitfall Avoidance Points (Important)

11. Summary

The java.util.function package is the core cornerstone of Java 8 functional programming. All Lambda and Stream underlying layers rely on these built-in interfaces.

Mastering these interfaces allows you to completely say goodbye to redundant anonymous inner classes, write more concise, elegant, and efficient stream-based code, and easily handle the high-frequency questions about Java 8 functional programming in interviews.

The essence of Java 8 functional programming is not syntactic sugar, but the programming idea of behavior parameterization. Proficiently using the various interfaces under the function package can greatly enhance code reusability and flexibility, which is an essential core skill for backend developers.