上周有个读者找我复盘面试,说考官抛了个问题:“Java四大核心函数式接口,分别是什么?各自用在什么场景?”他一听挺高兴,张口就来:Supplier是生产数据的,Consumer是消费数据的,Function是转换数据的,Predicate是判断数据的。结果考官追问了一句:“那Stream的filter方法里面,传进去的是哪个?”他一下就有点懵,嘴里开始含糊,最后草草收场。其实这就是典型的“名字记住了,签名没记牢”。
这种情况我见得不少。Supplier、Consumer、Function、Predicate这四个接口,算是Java函数式编程的四大基石,lambda表达式靠它们才能找到一个“类型”承载自己,Stream API里几乎每一个操作背后都有它们的影子。搞懂这四个接口,不光面试时能扛住追问,平时写代码也能把大量重复模板压下去,让逻辑真正变成“可传递的行为”。
这篇文章我会从设计思路、源码细节、完整案例、坑位清单四个角度展开。无论你是准备java面试的在校生,还是用了两年Stream但只知其然的工作党,都建议完整看一遍。看完你会发现,函数式接口没什么玄乎的,就是四个动作模板。
1. 内容整体设计与思路拆解
1.1 为什么Java 8需要函数式接口:从匿名内部类的痛点说起
Java 8之前,想在代码里“传一段行为”,只能靠匿名内部类。最典型的就是排序:
java复制Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
真正有业务价值的只有最后一行 a.length() - b.length(),前面全是模板代码。如果这段逻辑放在循环里再嵌套几层,可读性直接崩盘。Java 8引入了lambda表达式,这个问题从“写法”上解决了:
java复制Collections.sort(list, (a, b) -> a.length() - b.length());
但这里有个关键问题:lambda表达式本身没有类型,它必须被赋值给一个接口类型才能使用。这个接口不能是随便什么接口,而必须是“只有一个抽象方法”的接口,也就是SAM接口(Single Abstract Method)。Java 8在JDK里预置了一批高频使用的SAM接口,并且用 @FunctionalInterface 注解做编译期校验,这四个核心函数式接口就是其中最常用的代表。
所以可以这么理解:lambda解决的是“怎么写更简洁”,函数式接口解决的是“往哪个类型里塞”。两个东西配合,才把函数式编程的底子真正铺起来了。
1.2 四大接口的定位对比:一张表看懂“生产、消费、转换、判断”
很多初学者把四个接口搞混,就是因为没抓住核心差异。我建议直接看方法签名,签名决定了一切:
| 接口 | 唯一抽象方法 | 签名特征 | 形象比喻 | 典型使用位置 |
|---|---|---|---|---|
Supplier<T> |
T get() |
无参有返回值 | 供货商 | Stream.generate()、Optional.orElseGet() |
Consumer<T> |
void accept(T t) |
有参无返回值 | 消费者 | Iterable.forEach()、Stream.forEach() |
Function<T, R> |
R apply(T t) |
有参有返回值 | 加工流水线 | Stream.map()、Comparator.comparing() |
Predicate<T> |
boolean test(T t) |
有参返回布尔 | 质检员 | Stream.filter()、Collection.removeIf() |
只看返回值最方便:返回void就是Consumer,返回boolean就是Predicate,既传参又返回普通对象就是Function,什么都不传只返回就是Supplier。
这四个接口还有一个共同特点:都标注了 @FunctionalInterface。这个注解不是语法强制要求,但是强烈建议自定义函数式接口时也加上它,因为编译器会帮你校验“是不是真的有且只有一个抽象方法”。如果加了注解之后有人误加了第二个抽象方法,直接编译报错。
1.3 怎么快速选定接口:一个口诀和两次“有没有”自问
实际写代码时,遇到“该用哪个接口”的纠结,我一般这样快速判断:
先看有没有入参。没有入参,直接锁定Supplier;有入参,继续看有没有返回值。没有返回值,就是Consumer;有返回值,再继续看返回值类型。返回值是boolean,就是Predicate;返回值是其他类型,就是Function。
这个流程压缩成口诀就是六个字:无中生有找Supplier,只进不出找Consumer,非真即假找Predicate,又进又出找Function。
还有一个经验:如果判断过程中发现有两个入参,别急,JDK也提供了对应的二元版本——BiConsumer<T, U>、BiFunction<T, U, R>、BiPredicate<T, U>。至于二元版本的Supplier,逻辑上不存在,因为没有入参就是统一形态,不需要再区分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 Supplier详解:无中生有的“供货商”
Supplier的源码极度简洁,整个接口就一个抽象方法:
java复制@FunctionalInterface
public interface Supplier<T> {
T get();
}
它不关心你给不给参数,只负责返回一个结果。这种“无中生有”的特性最适合三类场景。
第一类是延迟加载。典型的例子是日志框架。假设要输出一条包含订单详情的debug日志,常规写法是:
java复制if (logger.isDebugEnabled()) {
logger.debug("订单详情:" + buildDetail(order));
}
先判断日志级别再拼字符串,是因为 buildDetail(order) 可能开销很大,不能让它在非debug级别下白白执行。用Supplier可以把这个逻辑包装得更优雅(slf4j支持Supplier重载):
java复制logger.debug("订单详情:{}", () -> buildDetail(order));
字符串拼接和查询动作只会在真正需要输出时执行,代码从“调用方手动判断”变成了“行为延迟触发”。
第二类是Optional的兜底逻辑。Optional.orElse 和 orElseGet 的区别经常被拿来当面试题,其实核心就是Supplier的应用场景:
j复制
