大家好,我是晚安code。
翻 Spring 源码,@FunctionalInterface 一搜几百处,再看 MyBatis、Netty 也一样。Java四大函数式接口早就不只是面试题,而是框架底层的心跳。可我刚学那会儿,看完 lambda 只会在 new Runnable 里打转,直到有人点破:lambda 得有插槽接住,Stream 只是把插槽串成流水线。
这篇把 Function、Predicate、Consumer、Supplier 四个接口从源码签名拆到业务写法,再讲 Stream 流式计算怎么把它们串成一行链式代码。全文读完约 10 分钟,建议先点收藏再往下看。
(2026 年 8 月整理,适用于 Java 8 及以上版本,接口签名在 JDK 17 长期支持版依旧一致。)
一、函数式接口:lambda 表达式落地的「接收槽」
函数式接口是整个函数式编程的入口,看不懂它,后面所有 Stream 代码都是玄学。
先说 lambda。Java 8 之前,你想把一段行为传给方法,只能 new 一个匿名内部类:
// Java 7 及以前的写法:匿名类(示例)
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("任务跑完了");
}
};
写一次还能忍,写多了手是真的酸。lambda 把这个过程压成一行:
// lambda:参数 -> 实现(示例)
Runnable r2 = () -> System.out.println("任务跑完了");

可 lambda 再短,编译器也得知道「这坨行为」是什么类型——谁来接住它?答案就是函数式接口。
Lambda 表达式(Lambda Expression):Java 8 引入的匿名函数写法,用 -> 把参数列表和方法体连成一句。你可以理解为「随手写在代码里的匿名方法」。
函数式接口(Functional Interface):只声明一个抽象方法的接口,用 @FunctionalInterface 标注,是 lambda 表达式的接收类型。你可以理解为「预先挖好的插槽,lambda 是插头」。
判断一个接口是不是函数式接口,只看它抽象方法是不是只有一个:Runnable 只有一个 run(),Comparator 只有一个 compare(),都是。这四个接口长什么样?下面逐个拆。

二、Function 函数型接口与 Predicate 断定型接口:一个转换、一个判定
Function 和 Predicate 是四大接口里出场率最高的两个,前者负责「变形」,后者负责「点头或摇头」。
Function 函数型接口:接收一个输入参数 T、返回一个结果 R 的函数式接口,抽象方法是 R apply(T t)。你可以理解为「输入原料、吐出成品的机器」。
源码签名长这样(JDK 里还有 compose、andThen 等默认方法,核心就这一个抽象方法):
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
lambda 里只写 (str) -> str 是把参数原样返回,换到业务上才看得出价值——比如给商品价格算含税价:
// 输入商品价格,输出含 13% 增值税的售价(示例)
Function<Double, String> withTax = price ->
String.format("¥%.2f", price * 1.13);
System.out.println(withTax.apply(100.0)); // ¥113.00
System.out.println(withTax.apply(258.0)); // ¥291.54
Function<String, Integer> 就是「字符串转整数」这类活,和 Stream 的 map 天生一对——把每个元素变个形。
Predicate 断定型接口:接收一个输入参数 T、只返回 boolean 的函数式接口,抽象方法是 boolean test(T t)。你可以理解为「一个只会回答是或否的考官」。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
它最典型的用法是当条件函数——比如判断订单金额够不够免运费门槛:
// 满 99 元免运费(示例)
Predicate<Double> freeShipping = amount -> amount >= 99.0;
System.out.println(freeShipping.test(128.0)); // true
System.out.println(freeShipping.test(56.0)); // false
记一个点:Stream 的 filter() 里塞的正是 Predicate——考官点头,元素留下;摇头,元素走人。
我第一次见到 apply、test 这种抽象方法名,觉得像黑话,后来把它们想成机器的进料口和出料口,一下就通了。

三、Consumer 消费型接口与 Supplier 供给型接口:只进不出与只出不进
Consumer 和 Supplier 是一对方向相反的接口,一个只进不出负责「处理」,一个只出不进负责「生产」。
Consumer 消费型接口:接收一个输入参数 T、没有返回值的函数式接口,抽象方法是 void accept(T t)。你可以理解为「只吃不吐的吃货」。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
没有返回值,所以它最适合做「副作用」操作——打印、写日志、入库:
// 把日志打到控制台,只消费不返回(示例)
Consumer<String> log = msg -> System.out.println("[日志] " + msg);
log.accept("订单已创建,订单号:ORD-20260811");
Stream 里的 forEach 接收的就是 Consumer,这也是为什么 forEach(System.out::println) 能直接传方法引用——println 的参数类型和 accept 完全对上。
Supplier 供给型接口:没有输入参数、只返回一个结果 T 的函数式接口,抽象方法是 T get()。你可以理解为「一个只吐不吃的自动售货机」。
@FunctionalInterface
public interface Supplier<T> {
T get();
}
典型场景是「现在先不急着算,需要时再取」——生成订单号、给缺省字段兜底:
// 生成订单号,只出不进(示例)
Supplier<String> orderNo = () ->
"ORD-" + System.currentTimeMillis();
System.out.println(orderNo.get()); // 形如 ORD-1723348800000
Stream 的 generate(Supplier) 也是拿它当原料来源。
四、四大函数式接口对比:一张表看懂谁进谁出
把四个接口的签名放一起看,记住「进、出」两个字就够了。
| 接口 | 抽象方法 | 输入 | 输出 | 典型场景 |
|---|---|---|---|---|
| Function | R apply(T t) | 有 | 有 | 类型转换、字段加工 |
| Predicate | boolean test(T t) | 有 | boolean | 条件筛选 |
| Consumer | void accept(T t) | 有 | 无 | 打印、写日志 |
| Supplier | T get() | 无 | 有 | 生成默认值、工厂 |
看这张图,四个接口的数据流向一眼就清楚——箭头朝里是有输入,箭头朝外是有输出:

这个「进出」记牢,后面看 Stream 的每一环就不慌了。
五、Stream 流式计算:存储交给集合,计算交给流
集合和 MySQL 负责「存」,Stream 负责「算」,把这两件事拆开,是 Java 8 处理数据最值钱的思想。
Stream 流(Stream API):Java 8 提供的对集合元素的函数式处理管道,本身不存数据,只负责「遍历 + 加工 + 汇聚」。你可以理解为「货架上的货,一条传送带送过去逐个处理」。
传统写法里,过滤、排序、转换全靠 for 循环手搓,循环里还容易顺手改到共享变量。Stream 把「干什么」和「怎么循环」拆开,你只要描述每一步做什么,循环细节交给它。

Stream 的操作分两类,这个区别最容易被忽略:
1)中间操作(Intermediate):filter、map、sorted、limit、distinct……调用后返回新的 Stream,属于「先记下来要做」;
2)终止操作(Terminal):forEach、collect、count、reduce……调用后整条链才开始真正执行。
Stream 是懒的:中间操作只是「报名」,最后一行终止操作才「开跑」。我最早写 demo 时跑了一整个方法体都没看到输出,还以为是 JDK 出问题了,后来才意识到链尾压根没写终止操作。
可能有人会问:中间操作和终止操作不都是写一行代码吗,这区别有那么重要吗?
重要。中间操作全部懒执行,你写
.filter(...).map(...)却不写终止操作,一条数据都不会动;只有forEach或collect出现,前面的步骤才会按顺序真正跑一遍。排查「为什么 stream 没生效」的第一思路,就是先看链尾有没有终止操作。
把这条处理链画出来,流程是这样的——中间操作逐级接力,最后一步终止操作触发整条链:

六、Stream 流式计算实战:一行链式代码搞定过滤映射排序
Stream 的价值不是替代 for 循环,而是把「过滤、映射、排序、截断」变成一串可读性极强的链式调用。
上个真实点的场景:手里有一批订单,想找出「已支付且金额满 500」的商品,名字转大写,再按名字倒序,只取前两名。Order 是带 getAmount()、getPayStatus()、getName() 的简单 POJO(示例):
List<Order> orders = Arrays.asList(
new Order(1, "Phone", 5999, 1),
new Order(2, "Keyboard", 399, 0),
new Order(3, "Monitor", 1299, 1),
new Order(4, "Headset", 699, 1),
new Order(5, "Charger", 89, 1)
);
orders.stream()
.filter(o -> o.getAmount() >= 500) // 满 500
.filter(o -> o.getPayStatus() == 1) // 已支付
.map(o -> o.getName().toUpperCase()) // 名字转大写
.sorted((a, b) -> b.compareTo(a)) // 倒序
.limit(2) // 只取前 2
.forEach(System.out::println); // 终止操作,触发执行
运行结果只有两行:
PHONE
MONITOR
一步一步看:两个 filter 先把 Keyboard 和 Charger 请出去,剩 Phone、Monitor、Headset;map 转大写;sorted 倒序后是 PHONE、MONITOR、HEADSET;limit(2) 掐掉后面的,forEach 打印前两个。

你有没有发现,这条链的每个环节,前面学的接口都在接单:filter 收 Predicate,map 收 Function,forEach 收 Consumer,sorted 收 Comparator(它本身也是个函数式接口)。Java四大函数式接口不是四个孤立的概念,而是这条流水线上每个工位的「接单员」——这就是函数式接口 + Stream 流式计算的组合拳。
七、一个高频疑问 + 小结
可能有人会问:Stream 流式计算比 for 循环快吗?
别指望它带来性能提升。大多数场景下 Stream 和 for 循环速度接近,甚至因为多了对象和闭包开销会略慢一点。Stream 的价值是「写起来清爽、语义清晰、不易改错」,不是更快;真要优化性能,着眼点应该是算法本身,而不是换成哪种循环写法。
看懂四个接口的「进出」,你就能读懂八成框架源码里的链式代码;再上手 Stream 流式计算,集合处理基本告别 for 循环。Java四大函数式接口这一套,是我现在处理集合数据的主旋律。
我第一次把 for 循环改成 Stream 链时,也被「怎么一下就变这么长了」吓到过,但一旦习惯,再回头写 for 循环是真的不想写了。学不完,真的学不完,但每弄懂一个,写代码就少怕一个。

我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你第一次用 Stream 流式计算写集合处理是什么时候?踩过哪些坑?