系列:Lambda 函数式实战精通营 · 作者:鱼宵
配套课程仓库:https://gitee.com/j67mk2/lambda-journey (第 2 课)
先看一个每天都在发生的现场
领导让你把"高薪研发部员工"做成一张报表:筛出研发部且月薪 ≥15000 的,拼成一行字打出来。
第一次写代码的人,会这样:
for(Employeee:team){if(e.dept().equals("研发")&&e.salary()>=15000){// 条件写死在循环里System.out.println(" [报表] ★"+e.name()+"|研发部|"+e.salary()+"元/月★");}}代码不多,但问题藏在后面:明天要改成"筛测试部",后天要"筛行政部且工资 5000 以上",再后天要在打印前加个通知动作——每次都要动这段循环。条件、转换、打印全焊死在一起,改一处动全身。
要是能把"校验、转换、消费"拆成三段,每段独立换——这就是本文要讲的:JDK 预置的四大函数式接口 + 组合方法。
学完本文你能回答:Predicate、Function、Consumer、Supplier 分别管什么?andThen 和 compose 有什么区别、谁先算谁?为什么说函数式接口可以像乐高一样"拼装"?
一句话原理:四个接口各管一段,组合方法把它们拼成流水线
先摆结论:
- Predicate<T>:断真假。抽象方法
boolean test(T t)——传一个东西进去,回答"满不满足条件",只还 true/false; - Function<T,R>:变数据。抽象方法
R apply(T t)——传一个类型进去,还回来另一个类型; - Consumer<T>:只动手。抽象方法
void accept(T t)——传一个东西进去,做一件事(打印/落库/发通知),什么都不还; - Supplier<T>:只供货。抽象方法
T get()——不给它任何输入,它负责"造一个出来"。
组合方法(default 方法)能把小函数拼成大函数:Predicate有and(相当于 &&)、or(相当于 ||)、negate(相当于 !);Function有andThen、compose;Consumer有andThen。
类比:乐高积木。单个积木(单个函数)只能干一件小事,凹凸卡扣一拼(组合方法),就能拼出城堡(流水线)。
完整可运行代码:一条员工报表流水线
直接复制就能跑(JDK 21+):
packagecom.lambda.lesson02;importjava.util.List;importjava.util.function.Consumer;importjava.util.function.Function;importjava.util.function.Predicate;importjava.util.function.Supplier;publicclassMain{/** 员工:姓名 + 年龄 + 月薪 + 部门 */recordEmployee(Stringname,intage,intsalary,Stringdept){}publicstaticvoidmain(String[]args){System.out.println("========== 一、Predicate:断真假(test / and / or / negate) ==========");Predicate<Integer>adult=age->age>=18;// 满 18 岁?Predicate<Integer>retired=age->age>=60;// 满 60 岁?System.out.println("20 岁成年?"+adult.test(20));// trueSystem.out.println("20 岁退休?"+retired.test(20));// false// and:两个条件都要满足(相当于 &&);negate:取反(相当于 !)Predicate<Integer>middleAge=adult.and(age->age<60);// 成年但没退休 = 中年System.out.println("40 岁算中年?"+middleAge.test(40));// trueSystem.out.println("70 岁算中年?"+middleAge.test(70));// falseSystem.out.println("70 岁退休?"+retired.test(70));// trueSystem.out.println("40 岁没退休?"+retired.negate().test(40));// true// or:两个条件满足一个就行(相当于 ||)Predicate<Integer>notCareer=retired.or(age->age<22);System.out.println("19 岁还算职业期?"+notCareer.test(19));// trueSystem.out.println("35 岁还算职业期?"+notCareer.test(35));// falseSystem.out.println();System.out.println("========== 二、Function:变数据(apply / andThen / compose) ==========");Function<Integer,Integer>plus2=x->x+2;// 加 2Function<Integer,Integer>times3=x->x*3;// 乘 3System.out.println("plus2.apply(5) = "+plus2.apply(5));// 7// andThen:先算自己,再拿结果接着算下一个——f.andThen(g) = 先 f 后 gFunction<Integer,Integer>plus2ThenTimes3=plus2.andThen(times3);System.out.println("(5+2)*3 = "+plus2ThenTimes3.apply(5));// 21// compose:先算下一个,再拿结果算自己——f.compose(g) = 先 g 后 fFunction<Integer,Integer>times3AfterPlus2=plus2.compose(times3);System.out.println("(5*3)+2 = "+times3AfterPlus2.apply(5));// 17System.out.println();System.out.println("========== 三、Consumer:只动手(accept / andThen) ==========");Consumer<String>printBox=s->System.out.println(" [盒装] "+s);Consumer<String>printStar=s->System.out.println(" [星标] "+s);printBox.accept("第一份");// andThen:把两个 Consumer 串成"先做 A 再做 B"Consumer<String>boxThenStar=printBox.andThen(printStar);System.out.println("组合 Consumer 处理\"第二份\":");boxThenStar.accept("第二份");System.out.println();System.out.println("========== 四、Supplier:只供货(get) ==========");Supplier<String>defaultTitle=()->"未命名文档-"+System.currentTimeMillis()%100000;Supplier<Integer>zero=()->0;System.out.println("默认标题:"+defaultTitle.get());System.out.println("兜底数字:"+zero.get());System.out.println();System.out.println("========== 五、综合:员工报表流水线(供给→校验→转换→消费) ==========");// 四个接口串成流水线:Supplier 供货,Predicate 校验,Function 转换,Consumer 消费Supplier<List<Employee>>supplyTeam=()->List.of(newEmployee("张三",28,18000,"研发"),newEmployee("李四",32,12000,"测试"),newEmployee("王五",26,23000,"研发"),newEmployee("赵六",45,9000,"行政"));// 校验:研发部 且 月薪 >= 15000Predicate<Employee>isRnd=e->e.dept().equals("研发");Predicate<Employee>wellPaid=e->e.salary()>=15000;Predicate<Employee>target=isRnd.and(wellPaid);// 转换:员工 → 一行报表文字,再 andThen 给名字加星标Function<Employee,String>toLine=e->e.name()+"|"+e.dept()+"部|"+e.salary()+"元/月";Function<String,String>starName=line->"★"+line+"★";Function<Employee,String>toReport=toLine.andThen(starName);// 消费:打印报表行Consumer<String>reportOut=line->System.out.println(" [报表] "+line);System.out.println("开始生成高薪研发名单:");intcount=0;for(Employeee:supplyTeam.get()){// Supplier.get() 供货if(target.test(e)){// Predicate.test() 校验Stringline=toReport.apply(e);// Function.apply() 转换reportOut.accept(line);// Consumer.accept() 消费count++;}}System.out.println("共筛出 "+count+" 人(张三、王五是研发且高薪;李四在测试部不沾边;赵六是行政部且工资只有 9000)");}}真实运行输出(本机 JDK 24.0.1 实测):
========== 一、Predicate:断真假(test / and / or / negate) ========== 20 岁成年?true 20 岁退休?false 40 岁算中年?true 70 岁算中年?false 70 岁退休?true 40 岁没退休?true 19 岁还算职业期?true 35 岁还算职业期?false ========== 二、Function:变数据(apply / andThen / compose) ========== plus2.apply(5) = 7 (5+2)*3 = 21 (5*3)+2 = 17 ========== 三、Consumer:只动手(accept / andThen) ========== [盒装] 第一份 组合 Consumer 处理"第二份": [盒装] 第二份 [星标] 第二份 ========== 四、Supplier:只供货(get) ========== 默认标题:未命名文档-35079 兜底数字:0 ========== 五、综合:员工报表流水线(供给→校验→转换→消费) ========== 开始生成高薪研发名单: [报表] ★张三|研发部|18000元/月★ [报表] ★王五|研发部|23000元/月★ 共筛出 2 人(张三、王五是研发且高薪;李四在测试部不沾边;赵六是行政部且工资只有 9000)这段代码讲什么
四个接口的定位:背抽象方法签名就够了
| 接口 | 抽象方法 | 一句话定位 | 类比 |
|---|---|---|---|
| Predicate<T> | boolean test(T t) | 断真假 | 门卫查证件,只回"放行/不放行" |
| Function<T,R> | R apply(T t) | 变数据 | 食堂窗口,递饭票出盖饭 |
| Consumer<T> | void accept(T t) | 只动手 | 快递柜投递格口,收走不还 |
| Supplier<T> | T get() | 只供货 | 自动售货机,按一下吐一瓶水 |
面试被问"四大函数式接口",先甩这张表,再补例子。
andThen vs compose:执行顺序相反(面试高频送命题)
Function<Integer,Integer>plus2=x->x+2;Function<Integer,Integer>times3=x->x*3;plus2.andThen(times3).apply(5)// (5+2)*3 = 21,先 plus2 后 times3plus2.compose(times3).apply(5)// (5*3)+2 = 17,先 times3 后 plus2- andThen:先算自己,结果"然后"给下一个。记"从左往右读"——
plus2.andThen(times3)读成"先加 2,再乘 3"; - compose:先算下一个,结果再"垫脚"给自己。记"从右往左算"——
plus2.compose(times3)读成"先乘 3,再加 2"; - 两者互为镜像:任何
f.compose(g)都等于g.andThen(f),选哪个纯看读着顺。
类比:给奶茶加料。andThen 是"先加珍珠再加奶盖";compose 是"先加奶盖再加珍珠"——加的料一样,顺序不一样,最后口味(结果)就不一样。
组合方法不修改原函数,而是返回新函数
Predicate<Integer>middleAge=adult.and(age->age<60);and内部大致是return t -> test(t) && other.test(t);——它返回的还是一个 lambda,原adult纹丝不动;- 新手常犯的错:只写
adult.and(...)不接返回值,以为"生效了"——没接住返回值,新函数就丢了。组合方法本质是"帮你 new 一个组合后的 lambda",必须接住返回值。
流水线的价值:四段独立,换一段不动其他
Supplier<List<Employee>>supplyTeam=...// 供货:换名单来源只改这Predicate<Employee>target=isRnd.and(wellPaid);// 校验:换筛选条件只改这Function<Employee,String>toReport=...// 转换:换输出格式只改这Consumer<String>reportOut=...// 消费:换去向(打印/落库/通知)只改这- 明天要把"研发"改成"测试":只动
isRnd那一行; - 后天要把名单从内存换成数据库:只动
supplyTeam那个 lambda; - 每段能单独测、单独换,谁也不知道别人的内部细节——这就是"分工"。
📌 进阶深挖(选学)
- 官方接口不止四个:
java.util.function包还有BiFunction<T,U,R>(两个输入)、BiPredicate、BiConsumer、UnaryOperator(Function<T,T> 特例)、BinaryOperator(两个 T 进一个 T 出),还有避免装箱的原始类型版本:IntPredicate、LongFunction、DoubleSupplier(第 9 课讲性能时你就知道为什么要专门为 int 开一套)。 - 组合方法是 default 方法的原因:Java 8 为了"给老接口加新方法又不破坏实现类"的妥协——加了 default 方法,老实现类不用改也能用新能力。
- 设计模式视角:
andThen层层包装,就像给奶茶加珍珠、加奶盖、加冰——每层都不改变"奶茶本体",只是在外面再套一层加工。调用时从最外层往里算,和装饰器模式"层层包装、层层委托"是同一个思想。
面试回答模板
问:andThen 和 compose 有什么区别?
- 一句话结论:两者都是 Function 的组合方法,但执行顺序相反。
- 展开 1~2 句:
f.andThen(g)是先执行 f 再把结果交给 g,即g(f(x));f.compose(g)是先执行 g 再交给 f,即f(g(x))。拿plus2和times3代 5 手算:andThen 得 21,compose 得 17。 - 收一句:拼数据处理流水线时用 andThen 读着最顺,和"一步接一步"的写法一致(指向课程 lesson-02 §8)。
问:Predicate 和 Function 分不清怎么办?
- 一句话结论:有返回值的是 Function,没有的是 Consumer;Predicate 是"返回 boolean 的 Function"。
- 展开:
Predicate.test只回答"满不满足条件"(boolean),Function.apply把输入变成另一个类型(T→R),Consumer.accept做一件事什么都不还,Supplier.get连入参都没有、只负责造东西(指向课程 lesson-02 §3)。
总结表:坑 → 原因 → 解法
| 坑 | 原因 | 解法 |
|---|---|---|
| 四个接口记混 | 签名分不清 | 背"有返回是 Function,无返回是 Consumer,只答真假是 Predicate,只出不进是 Supplier" |
| andThen/compose 顺序写反 | 把 andThen 当成"先乘后加" | 代入具体数字手算:5 代进去,21 和 17 谁是哪个立刻清楚 |
| 以为组合方法修改了原函数 | 忽略"返回新函数" | f.andThen(g)必须接住返回值,原f纹丝不动 |
a.or(b).and(c)优先级想当然 | 组合按调用顺序从左往右粘 | a.or(b).and(c)是(a‖b)&&c,想要别的逻辑就用变量分层 |
| JDK 低于 21 | 版本不满足 | 先java -version确认 21+ |
挑战题(答案都在课程源码里)
- ⭐ 把员工名单扩到 6 个人,工资阈值从 15000 改成 20000,重新跑,看报表剩几行。
- ⭐⭐ 把
isRnd.and(wellPaid)改成isRnd.or(wellPaid),猜一下输出会变几行,跑起来验证。 - ⭐⭐ 用
negate()写一条规则"筛出不满足’研发且高薪’的员工",打印被刷掉的人是谁。 - ⭐⭐⭐ 加一段"发通知"流程:再定义一个
Consumer<Employee>(打印"通知已发:张三"),用 andThen 把"打印报表行"和"发通知"串起来,让每个被选中的员工同时触发两个动作。
源码在哪,怎么跑
完整代码在lambda-journey 课程第 2 课:
gitclone https://gitee.com/j67mk2/lambda-journey.gitcdlambda-journey/lesson-02 mvn-qcleaninstall-DskipTestsjava-cptarget/classes com.lambda.lesson02.Main- 第 2 课:四大接口 + 组合方法全解 + 5 道面试题 + 课后作业 5 道
本文属于Lambda 函数式实战精通营系列(10 课):L1 世界观 →L2 四大接口→ L3 方法引用 → L4 闭包 → L5 异常 → L6 重构 → L7/L8 Lambda×Stream 组合 → L9 性能 → L10 企业级收官。下一篇:方法引用四种形态——
::到底能引用哪四种方法,构造器怎么用::引出来。
跑出来的输出和我不一样?把现象发评论区,一起查。觉得有用就收藏一下,追更这个系列。