函数式编程:用BiFunction消除多类型分支的代码重复

下面拿一个我在生产环境用过的一个库存变更的场景作为例子,演示一下如何使用JDK自带的BiFunction。也是函数编程的一种使用。

问题场景

一个库存日报表模块,支持三种变更类型:期初库存、在途库存、入库库存。每种类型对应DailyInventoryRecord上一个不同的更新方法:updateOpeningInventoryupdateInTransitInventoryupdateReceivedInventory

业务逻辑分两条路径:新增一条库存记录,或者更新已有记录。两条路径里都要根据类型判断该调哪个方法。

先看新增路径:

Java 复制代码
if (TYPE_OPENING.equals(type)) {
    record.updateOpeningInventory(param);
} else if (TYPE_IN_TRANSIT.equals(type)) {
    record.updateInTransitInventory(param);
} else if (TYPE_RECEIVED.equals(type)) {
    record.updateReceivedInventory(param);
}

更新路径里,一模一样的if-else再来一遍,只是record对象换了个变量名。

问题出在哪?两个路径里的if-else结构完全一样,只是调用位置不同。如果后面要加一种「出库库存」类型,新增路径要改,更新路径也要改。改两处本身不费事,但容易漏。

能不能把「调哪个方法」这个决策提前做一次,后面的两条路径直接拿着结果去调用?

解法:switch表达式 + BiFunction + 方法引用

BiFunction是JDK 8引入的函数式接口,在java.util.function包下,接收两个参数,返回一个结果。用在这个场景刚好合适:更新方法需要接收记录对象和变更参数两个入参,返回更新后的记录对象。

核心就这几行:

Java 复制代码
BiFunction<DailyInventoryRecord, InventoryChangeParam, DailyInventoryRecord> fn = switch (type) {
    case TYPE_OPENING -> DailyInventoryRecord::updateOpeningInventory;
    case TYPE_IN_TRANSIT -> DailyInventoryRecord::updateInTransitInventory;
    case TYPE_RECEIVED -> DailyInventoryRecord::updateReceivedInventory;
};

这里用了Java 14引入的switch表达式配合方法引用。DailyInventoryRecord::updateOpeningInventory是一个未绑定的方法引用,编译器会自动把调用对象本身当作第一个参数。所以它的实际签名是(DailyInventoryRecord, InventoryChangeParam) -> DailyInventoryRecord,和BiFunction的泛型完全匹配。

有了这个fn,新增和更新两条路径都不需要再写if-else了。

新增路径里,buildNewRecords方法接收fn作为参数,内部统一调用:

Java 复制代码
// 构建新增记录
private List<DailyInventoryRecord> buildNewRecords(
        BiFunction<DailyInventoryRecord, InventoryChangeParam, DailyInventoryRecord> fn,
        Set<String> newMaterialCodes, Map<String, InventoryChangeParam> paramMap) {
    return newMaterialCodes.stream().map(code -> {
        DailyInventoryRecord record = new DailyInventoryRecord(code);
        return fn.apply(record, paramMap.get(code));
    }).toList();
}

更新路径更直接,循环里调一下:

Java 复制代码
// 更新已有记录
for (DailyInventoryRecord record : existingRecords) {
    fn.apply(record, paramMap.get(record.getMaterialCode()));
}

两条路径的代码里没有任何类型判断,都是直接fn.apply()。if-else被集中到了fn赋值的那一个地方。

这种写法用起来很顺,但它能成立有一个硬性前提。

前提条件

能被同一个BiFunction统一的方法,入参个数、参数类型、返回值类型必须完全一致。

拿这个例子来说,三个更新方法都是接收一个InventoryChangeParam,返回DailyInventoryRecord自身。签名一致,才能用方法引用统一赋值。如果其中某个方法多了一个参数,或者返回类型不同,BiFunction的泛型就约束不住了,编译直接报错。

所以这不是一个万能的模式。方法签名不一致的时候,别硬往上套,老老实实用if-else反而更清晰。

回到整体来看,改前和改后的差异到底在哪?

改前改后对比

维度 改前(if-else分散) 改后(BiFunction统一)
类型判断代码 两条路径各写一遍if-else switch只写一次
新增类型时要改的地方 两个路径各改一处,共2处 switch里加一行,共1处
漏改风险 高,改一处容易忘改另一处 低,只有一个入口
使用路径的代码 每个路径内部都有分支逻辑 统一fn.apply(),无分支

判断要不要用这个模式的标准很具体:看那几个分支里调用的方法签名是否一致。 入参个数、参数类型、返回值都相同,用函数式接口统一就很自然。签名不一致,强统一反而要写额外的适配代码,不如保持if-else。

小结

Java的函数式接口在实际项目里最有用的地方,不是替代所有条件分支,而是处理「结构相同、方法不同」这一类重复代码。BiFunctionConsumerFunction这些接口在JDK里已经存在了很多年,问题不在于知不知道它们,而在于遇到合适场景的时候能不能想起来用。

判断标准只有一个:分支里各方法的签名能不能统一。能统一,用函数式接口收拢到一个变量里,调用方只管apply(),不用关心调的是哪个具体方法。不能统一,就保持if-else,别勉强。这种模式算不上什么高级技巧,用多了就成了习惯。

相关推荐
Python私教13 分钟前
从表格到管理系统:别先写页面,先补齐权限、流程和审计
数据库·后端·架构
余额瞒着我当琳16 分钟前
C++--深拷贝三件套 + swap + 写时拷贝 + vector 扩容 + reserve
java·开发语言·c++
thefool11226625 分钟前
翻转二叉树
java
北斗落凡尘1 小时前
LangGraph 入门实战(12)--使用MCP
后端·python·langchain
嗯哼python1 小时前
从 0 到 1 构建 AI 模拟面试系统:RAG + LangGraph 的工程实践
人工智能·面试·职场和发展
喜欢打篮球的普通人1 小时前
LLVM Backend Lowering 从入门到实战:把 IR 变成机器码的完整链路
android·java·数据库
ShineWinsu2 小时前
对于C++:布隆过滤器(bloomfilter)的详细解析
c++·面试·位图·位运算·布隆过滤器·海量数据·比特位
FL16238631292 小时前
室内易燃物识别易燃评估室内易燃程度识别分割数据集labelme格式1015张85类别
java·服务器·前端
jufeng13072 小时前
【系列:TDengine 工业物联网实战:从零搭起可运行系统 · 第 8 篇】
java·spring boot·时序数据库·tdengine
kyriewen2 小时前
我把 AI 写的并发请求控制器手写了一遍——3 个语义我当时根本讲不清
前端·javascript·面试