☰
四大函数式接口:断真假/变数据/只动手/只供货,怎么拼流水线
2026/10/24 5:41:04 网站建设 项目流程

系列: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+

挑战题(答案都在课程源码里)

  1. ⭐ 把员工名单扩到 6 个人,工资阈值从 15000 改成 20000,重新跑,看报表剩几行。
  2. ⭐⭐ 把isRnd.and(wellPaid)改成isRnd.or(wellPaid),猜一下输出会变几行,跑起来验证。
  3. ⭐⭐ 用negate()写一条规则"筛出不满足’研发且高薪’的员工",打印被刷掉的人是谁。
  4. ⭐⭐⭐ 加一段"发通知"流程:再定义一个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 企业级收官。下一篇:方法引用四种形态——::到底能引用哪四种方法,构造器怎么用::引出来。

跑出来的输出和我不一样?把现象发评论区,一起查。觉得有用就收藏一下,追更这个系列。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询