讲讲 Java 函数式接口(Functional Interface)?它与 Stream API 有什么关系?
在 Java 8 中,函数式接口(Functional Interface)和 Stream API 是两项最重磅的特性。它们相辅相成,共同开启了 Java 的函数式编程时代。
下面我们由浅入深,先拆解“函数式接口”,再探讨它与“Stream API”的亲密关系。
一、 什么是函数式接口(Functional Interface)?
1. 定义
函数式接口:有且仅有一个抽象方法(Abstract Method)的接口。
尽管只能有一个抽象方法,但它可以有:
- 任意多个默认方法(default method)
- 任意多个静态方法(static method)
- 重写自
Object类的公开方法(如equals、hashCode、toString)
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 链路中,函数式接口扮演的角色:
filter(emp -> emp.getAge() > 30)- 传入的 Lambda
emp -> emp.getAge() > 30实现了Predicate<Employee>接口。 - Stream API 内部会调用
predicate.test(emp)来判断元素是否保留。
- 传入的 Lambda
map(emp -> emp.getName())- 传入的 Lambda
emp -> emp.getName()实现了Function<Employee, String>接口。 - Stream API 调用
function.apply(emp)把员工对象转成了名字字符串。
- 传入的 Lambda
forEach(name -> System.out.println(name))- 传入的 Lambda(或方法引用
System.out::println)实现了Consumer<String>接口。 - Stream API 最终调用
consumer.accept(name)执行打印。
- 传入的 Lambda(或方法引用
四、 总结:为什么要这样设计?
- 解耦(Behavior Parameterization - 行为参数化):
Stream 管道(API 框架)本身只负责控制流(怎么循环、怎么多线程并行、怎么惰性求值)。至于“怎么过滤”、“怎么转换”,全部通过函数式接口作为参数传进来。这使得 Stream 框架极其灵活、可复用。 - 极简的语法:
正是因为有了函数式接口,我们才能用极其简练的 Lambda 表达式或方法引用(如Employee::getName)来写代码,告别了过去臃肿的for循环和if判断,极大地提升了可读性和开发效率。
右滑查看面试常问