Java短路选择器设计:pan-common 告别if-else的三种模式
Java短路选择器设计:告别if-else的三种模式
订单状态分发、事件路由、配置映射——这些场景下冗长的 if-else 链不仅难看,还会产生不必要的计算开销。本文分析
pan-common中的短路选择器体系,如何用链式 API 替代 if-else,同时保持延迟计算和类型安全。
一、场景:状态码路由的三种写法
假设你要根据用户角色返回不同的权限码,传统写法是这样的:
String role = getUserRole();
int permission;
if ("admin".equals(role)) {
permission = computeAdminPermission(); // 可能很耗时的操作
} else if ("user".equals(role)) {
permission = computeUserPermission();
} else if ("guest".equals(role)) {
permission = computeGuestPermission();
} else {
permission = 0;
}
三个问题:
- 即使角色是
"admin",其他分支的computeXxx方法也可能在编写时不小心被提前调用 - 分支多了之后,if-else 链难以阅读
- JDK 14 的 switch 表达式只能比较固定常量,无法处理复杂条件
pan-common(当前版本 2.0.6 / 3.0.6,Spring Boot 2.x / 3.x,JDK 17+)的 ChooseEq / ChooseFirst 体系就是为了解决这些问题。
二、环境准备
Maven 坐标:
<dependency>
<groupId>com.gitee.apanlh</groupId>
<artifactId>pan-common</artifactId>
<version>3.0.6</version> <!-- Spring Boot 3.x;2.x 项目用 2.0.6 -->
</dependency>
无额外依赖,ChooseEq 体系完全基于 JDK 内置类。
三、三种模式,覆盖不同场景
3.1 模式一:ChooseFirst — 任意布尔条件
最灵活的模式,支持任意 boolean 表达式作为条件。
// 替代 if-else 链
String result = ChooseFirst.create(role.equals("admin"), "管理员")
.when(role.equals("user"), "普通用户")
.when(role.equals("guest"), "访客")
.end("未知角色");
延迟计算版本(只有匹配到的分支才会执行):
// computeAdminValue() 只在 role 为 "admin" 时执行
Integer value = ChooseFirst.create(role.equals("admin"), () -> computeAdminValue())
.when(role.equals("user"), () -> computeUserValue())
.when(role.equals("guest"), () -> computeGuestValue())
.end(() -> 0);
空构造 + 后置分支:
// 先创建空选择器,后续动态添加条件
ChooseFirst<Integer> selector = ChooseFirst.<Integer>create()
.when(status == 200, 2000)
.when(status == 404, 4000)
.when(status == 500, 5000);
int code = selector.end(9999);
3.2 模式二:FixedChooseEq — 固定源比较
当你有一个固定的变量需要和多个常量比较时,这是最简洁的模式。
// 固定源为 status,后续每个 when 只需传目标值
String msg = ChooseEq.create(status)
.when("SUCCESS", "操作成功")
.when("FAIL", "操作失败")
.when("NOT_FOUND", "资源不存在")
.end("未知状态");
对比 switch 表达式:
// JDK 14+ switch 表达式
String msg = switch (status) {
case "SUCCESS" -> "操作成功";
case "FAIL" -> "操作失败";
case "NOT_FOUND" -> "资源不存在";
default -> "未知状态";
};
两者效果等价,但 ChooseEq 多了两个能力:
- 延迟计算:
when("SUCCESS", () -> computeSuccessMsg())确保不匹配的分支不会执行 - 动作模式:不需要返回值时,用
endVoid()执行默认动作
// 仅执行动作,无返回值
ChooseEq.create(eventType)
.when("USER_LOGIN", () -> handleLogin())
.when("USER_LOGOUT", () -> handleLogout())
.when("USER_REGISTER", () -> handleRegister())
.endVoid(() -> log.warn("未知事件: {}", eventType));
3.3 模式三:ExplicitChooseEq — 两个变量都不固定
当比较的双方都是运行时变量时,使用显式比较模式。
// leftVal 和 rightVal 都是运行时变量
String label = ChooseEq.create()
.when(leftVal, rightVal, "两者相等")
.when(leftVal, fallbackVal, "与备选值相等")
.end("都不匹配");
这在两个动态值的比较场景下很有用,比如协议版本协商:
// 客户端版本和服务端支持版本都是动态的
String compatibility = ChooseEq.create()
.when(clientVersion, serverVersion, "完全兼容")
.when(clientVersion, "1.0", "向下兼容")
.when(serverVersion, "2.0", "服务端已升级")
.end("需要版本检查");
四、核心设计解析
4.1 短路机制
所有选择器的核心逻辑只有一个判断:
// AbstractChooser.java
protected FuncCall<R> matchValueSupplier;
// ChooseFirst.java
protected void addCase(boolean condition, FuncCall<R> funcCall) {
if (this.matchValueSupplier == null && condition) {
this.matchValueSupplier = funcCall;
}
}
// FixedChooseEq.java
private void match(T target, FuncCall<R> funcCall) {
if (this.matchValueSupplier != null) {
return; // 已匹配,后续 when 全部忽略
}
if (Eq.autoEq(this.source, target)) {
this.matchValueSupplier = funcCall;
}
}
一旦 matchValueSupplier 被赋值(找到第一个匹配的分支),后续所有 when 调用直接返回,不会产生任何计算开销。
4.2 延迟计算
ChooseEq 的 when 方法接受两种参数:
| 方式 | 示例 | 求值时机 |
|---|---|---|
| 立即值 | .when("admin", 100) |
链式调用构建时就求值 |
| 延迟计算 | .when("admin", () -> computeValue()) |
调用 end() 时,只有匹配才执行 |
关键区别在于:
// 立即值:所有分支的 computeXxx() 都会被调用,无论是否匹配
Integer result = ChooseEq.create(role)
.when("admin", computeAdminValue()) // 立即执行
.when("user", computeUserValue()) // 立即执行
.end(0);
// 延迟计算:只有匹配的分支才会执行
Integer result = ChooseEq.create(role)
.when("admin", () -> computeAdminValue()) // 仅当 role="admin" 时执行
.when("user", () -> computeUserValue()) // 仅当 role="user" 时执行
.end(() -> 0); // 仅当无匹配时执行
4.3 类型推断桥接设计
Java 泛型在链式调用中存在类型推断困难。ChooseEq 通过一个无状态的桥接器解决:
// 门面类
public static FixedSourceChooser<T> create(T source) {
return new FixedSourceChooser<>(source); // 无状态,只携带 source
}
// 桥接器:第一次 when 确定 <R> 泛型
public static final class FixedSourceChooser<T> {
private final T source;
public <R> FixedChooseEq<T, R> when(T target, R value) {
// 此时 Java 编译器已经推断出 R 的类型
return FixedChooseEq.<T, R>create(this.source).when(target, value);
}
}
调用方不需要手动指定泛型参数:
// 不需要这样写
ChooseEq.<String, Integer>create("admin").when("admin", 100)
// 编译器自动推断
ChooseEq.create("admin")
.when("admin", 100) // R = Integer 自动推断
.end(0);
4.4 Eq.autoEq 智能比较
ChooseEq 内部使用的不是简单的 Objects.equals,而是 Eq.autoEq,它支持:
null安全比较:null == null返回true- 数组比较:自动识别数组类型并进行深度比较
- 枚举比较:按枚举实例或按名称
- 对象比较:基于
equals方法
public static <T> boolean autoEq(T value, T eqValue) {
if (value == null && eqValue == null) return true;
if (value == null || eqValue == null) return false;
// 自动识别数组类型进行深度比较
if (value.getClass().isArray()) return autoEqArray(value, eqValue);
return Eq.object(value, eqValue);
}
4.5 7 种结束方式
AbstractChooser 提供了丰富的结束方法,覆盖不同返回值需求:
// 1. end() — 无匹配返回 null
String r1 = ChooseEq.create(role).when("admin", "管理员").end();
// 2. end(defaultValue) — 无匹配返回固定默认值
String r2 = ChooseEq.create(role).when("admin", "管理员").end("访客");
// 3. end(FuncCall) — 无匹配时延迟计算默认值
String r3 = ChooseEq.create(role).when("admin", "管理员").end(() -> fetchDefaultRole());
// 4. end(RuntimeException) — 无匹配时抛出异常
String r4 = ChooseEq.create(role)
.when("admin", "管理员")
.end(new IllegalArgumentException("未知角色: " + role));
// 5. endVoid(Runnable) — 无匹配时执行动作(无返回值)
ChooseEq.create(event)
.when("LOGIN", () -> handleLogin())
.endVoid(() -> log.warn("未知事件"));
// 6. endOrDefault(defaultValue) — 匹配值为 null 时也返回默认值
String r6 = ChooseEq.create(role).when("admin", null).endOrDefault("默认值");
// 7. endOrDefault(FuncCall) — 匹配值为 null 时延迟计算
String r7 = ChooseEq.create(role).when("admin", null).endOrDefault(() -> "默认");
注意 end(defaultValue) 和 endOrDefault(defaultValue) 的区别:
end(defaultValue):只要有匹配(即使匹配值是null),就返回匹配值,不返回默认值endOrDefault(defaultValue):匹配值是null时也会返回默认值
这是一个容易踩坑的细节,根据你的业务语义选择。
五、与非空检查的对比
传统的非空值获取:
String value;
if (cache.get(key) != null) {
value = cache.get(key);
} else if (config.getDefault(key) != null) {
value = config.getDefault(key);
} else if (fallback.get(key) != null) {
value = fallback.get(key);
} else {
value = "";
}
用 ChooseFirst 改写:
String value = ChooseFirst.create(cache.get(key) != null, cache.get(key))
.when(config.getDefault(key) != null, config.getDefault(key))
.when(fallback.get(key) != null, fallback.get(key))
.end("");
或者更推荐用延迟计算避免重复查询:
String value = ChooseFirst.<String>create()
.when(cache.get(key) != null, () -> cache.get(key))
.when(config.getDefault(key) != null, () -> config.getDefault(key))
.when(fallback.get(key) != null, () -> fallback.get(key))
.end(() -> "");
六、线程安全声明
ChooseEq / ChooseFirst 及其构建器非线程安全,仅供单线程使用。每个链式调用创建新实例,不共享状态。如果在多线程场景中使用,请确保每个线程持有独立的实例。
这不是设计缺陷,而是刻意的取舍——短路选择器本身就是单次决策操作,多线程共享一个选择器没有语义意义。
七、局限性
ChooseEq 不是万能的,以下场景不适用:
- 范围判断(如
age >= 18 && age < 65):ChooseFirst可以但可读性不如 if-else - 多条件组合(如
A && B || C):短路选择器只支持单一布尔条件或相等比较 - 性能敏感的热点路径:链式调用会产生少量对象创建开销,在纳秒级热点路径上不如 if-else
如果你的场景是 JDK 14+ 的简单 switch 表达式能解决的,直接用 switch 即可——原生语法不需要额外学习成本。ChooseEq 的价值在于延迟计算、动作模式和动态条件这三个 switch 做不到的事情。
八、总结
pan-common 的短路选择器体系用不到 400 行代码,提供了一套完整的 if-else 替代方案:
- 三种模式覆盖固定源比较、显式比较、任意条件判断
- 延迟计算确保不匹配的分支不会产生计算开销
- 类型推断桥接让调用方无需手动指定泛型参数
- 7 种结束方式覆盖返回值、异常、动作等多种需求
更多推荐
所有评论(0)