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;
}

三个问题:

  1. 即使角色是 "admin",其他分支的 computeXxx 方法也可能在编写时不小心被提前调用
  2. 分支多了之后,if-else 链难以阅读
  3. 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 延迟计算

ChooseEqwhen 方法接受两种参数:

方式 示例 求值时机
立即值 .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 替代方案:

  1. 三种模式覆盖固定源比较、显式比较、任意条件判断
  2. 延迟计算确保不匹配的分支不会产生计算开销
  3. 类型推断桥接让调用方无需手动指定泛型参数
  4. 7 种结束方式覆盖返回值、异常、动作等多种需求

项目地址:https://gitee.com/apanlh/pan-common

更多推荐