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

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

问题场景

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

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

先看新增路径:

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

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

相关推荐
陈随易2 小时前
Postman 被曝自动往 Claude Code 和 Codex 装插件
前端·后端·程序员
AI你一生一世4 小时前
不止于时序存储:DolphinDB 如何成为 AI 时代的工业智能底座
java·人工智能·struts·时序数据库·工业智能·dolphindb·ai基础设施
云-缩影4 小时前
拆解 AI 图像生成应用的后端架构:ECS、OSS 和大模型 API 怎么配合
java·spring boot·后端
瑞克-桑切斯4 小时前
7、MySQL 慢查询排查与优化实战指南
java·服务器·数据库
考虑考虑5 小时前
Posrgresql恢复.backup文件
数据库·后端·postgresql
无奈何杨5 小时前
风控决策系统决策流,策略模式,权重、顺序、投票、最坏
后端
Patrick_Wilson7 小时前
Go 为什么需要 defer?从函数怎么收尾说起
后端·go·编程语言
苏三说技术7 小时前
DeepSeek Harness入驻B站,评论区被干成了大型插件集市!
后端
橙序员小站7 小时前
我一句话让 AI 把公众号工作台搭完了:部署实测记录
后端
Patrick_Wilson7 小时前
从文本输出理解 Go 方法与接口
后端·go·编程语言