基于本文回答
0
评论

讲讲 Java 函数式接口(Functional Interface)?它与 Stream API 有什么关系?

在 Java 8 中,函数式接口(Functional Interface)Stream API 是两项最重磅的特性。它们相辅相成,共同开启了 Java 的函数式编程时代。

下面我们由浅入深,先拆解“函数式接口”,再探讨它与“Stream API”的亲密关系。


一、 什么是函数式接口(Functional Interface)?

1. 定义

函数式接口:有且仅有一个抽象方法(Abstract Method)的接口。

尽管只能有一个抽象方法,但它可以有:

  • 任意多个默认方法(default method)
  • 任意多个静态方法(static method)
  • 重写自 Object 类的公开方法(如 equalshashCodetoString

2. @FunctionalInterface 注解

Java 8 引入了该注解。它的作用是编译器检查:如果一个接口贴了该注解,但写了两个抽象方法,编译器就会报错。
注意:这个注解是可选的,只要接口满足“只有一个抽象方法”的条件,它本质上就是函数式接口。

java
@FunctionalInterface
public interface MyCalculator {
    int calculate(int a, int b); // 唯一的抽象方法

    // 默认方法不影响函数式接口的定义
    default void printInfo() {
        System.out.println("这是一个计算器接口");
    }
}

3. 为什么需要它?—— 为 Lambda 表达式而生

在 Java 8 之前,要传递一段代码(行为),必须使用臃肿的匿名内部类
有了函数式接口后,Lambda 表达式(以及方法引用)就可以作为该接口的实例进行传递。

java
// 1. 传统的匿名内部类写法
MyCalculator plusOld = new MyCalculator() {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
};

// 2. Lambda 表达式写法(极大简化了代码)
MyCalculator plusNew = (a, b) -> a + b; 

4. Java 内置的四大核心函数式接口

为了不让开发者每次都自己定义接口,Java 在 java.util.function 包下内置了 40 多个常用的函数式接口。最核心的是以下四个:

接口名称 参数类型 返回类型 核心抽象方法 适用场景/通俗理解
Consumer<T> (消费型) T void accept(T t) 传入一个参数,消费掉(如:打印、保存数据),无返回值。
Supplier<T> (供给型) T get() 无需参数,生产/提供一个数据(如:工厂方法、获取随机数)。
Function<T, R> (函数型) T R apply(T t) 传入 T 类型,加工转换后返回 R 类型(如:类型转换、数据处理)。
Predicate<T> (断言型) T boolean test(T t) 传入一个参数,判断是否满足条件,返回布尔值(如:过滤数据)。

二、 函数式接口与 Stream API 的关系

如果说 Stream API 是流水线,那么 函数式接口就是流水线上的各个“加工阀门”

Stream API 的核心思想是“做什么,而不是怎么做”(声明式编程)。它提供了一系列高阶函数(如 filter, map, forEach 等),这些方法全部接受函数式接口作为参数

1. 紧密结合的体现(方法签名对照)

看一看 Stream 的常用方法签名,你会发现它们都要求传入内置的函数式接口:

  • Stream<T> filter(Predicate<? super T> predicate)
    • filter 接收一个 Predicate(断言)。
    • 作用:决定哪些元素能留下来。
  • <R> Stream<R> map(Function<? super T, ? extends R> mapper)
    • map 接收一个 Function(函数)。
    • 作用:把元素 T 转换成 R
  • void forEach(Consumer<? super T> action)
    • forEach 接收一个 Consumer(消费者)。
    • 作用:遍历并消费每个元素。
  • Optional<T> reduce(BinaryOperator<T> accumulator)
    • reduce 接收一个双参数同类型的函数式接口。
    • 作用:将元素两两组合,最终聚合成一个值。

三、 实战:结合 Stream API 的综合示例

我们通过一个例子,来看看函数式接口是如何与 Stream API 协同工作的。

假设我们有一个 Employee 员工类列表,我们想:筛选出年龄大于30岁的员工 -> 获取他们的名字 -> 打印出来。

java
import java.util.Arrays;
import java.util.List;

public class StreamDemo {
    public static void main(String[] args) {
        List<Employee> employees = Arrays.asList(
                new Employee("张三", 28),
                new Employee("李四", 35),
                new Employee("王五", 42)
        );

        // 链式调用
        employees.stream()
                // 1. filter 接收 Predicate<Employee> (断言:年龄 > 30)
                .filter(emp -> emp.getAge() > 30) 
                
                // 2. map 接收 Function<Employee, String> (转换:获取名字)
                .map(emp -> emp.getName()) 
                
                // 3. forEach 接收 Consumer<String> (消费:控制台打印)
                .forEach(name -> System.out.println(name));
    }
}

class Employee {
    private String name;
    private int age;
    // Constructor, Getters...
    public Employee(String name, int age) { this.name = name; this.age = age; }
    public String getName() { return name; }
    public int getAge() { return age; }
}

拆解上述 Stream 链路中,函数式接口扮演的角色:

  1. filter(emp -> emp.getAge() > 30)
    • 传入的 Lambda emp -> emp.getAge() > 30 实现了 Predicate<Employee> 接口。
    • Stream API 内部会调用 predicate.test(emp) 来判断元素是否保留。
  2. map(emp -> emp.getName())
    • 传入的 Lambda emp -> emp.getName() 实现了 Function<Employee, String> 接口。
    • Stream API 调用 function.apply(emp) 把员工对象转成了名字字符串。
  3. forEach(name -> System.out.println(name))
    • 传入的 Lambda(或方法引用 System.out::println)实现了 Consumer<String> 接口。
    • Stream API 最终调用 consumer.accept(name) 执行打印。

四、 总结:为什么要这样设计?

  1. 解耦(Behavior Parameterization - 行为参数化)
    Stream 管道(API 框架)本身只负责控制流(怎么循环、怎么多线程并行、怎么惰性求值)。至于“怎么过滤”、“怎么转换”,全部通过函数式接口作为参数传进来。这使得 Stream 框架极其灵活、可复用。
  2. 极简的语法
    正是因为有了函数式接口,我们才能用极其简练的 Lambda 表达式或方法引用(如 Employee::getName)来写代码,告别了过去臃肿的 for 循环和 if 判断,极大地提升了可读性和开发效率。
右滑查看面试常问