设计模式 04:为什么重载没能识别整箱预留
普通预留可以取 1 件;整箱预留要求一次取偶数件。订单和库存仍沿用 03 篇的单线程契约:拒绝无效请求后,两个对象的状态都不能改变。新增规则看起来只需为 Bundle 添一个重载;但调用方把它存为 ReservationChannel 时,同一个整箱对象取 1 件竟然成功了。
两个阶段,不能交换
Java SE 21 JLS §15.12.2.5规定:在可适用的方法里,编译时选择最具体的重载,确定后续分派所用的方法描述符。§15.12.4.4再讨论运行时定位要调用的实例方法;类实例方法的重写规则见 §8.4.8.1。运行时对象能决定所选签名的哪个实现执行,却不会重新挑一个参数类型更窄的重载。
Before 的两条路径都是真实编译并执行过的代码,区别只在调用处的变量声明类型:
1 | |
第一行中的实际对象仍是 Bundle,但编译器只能按形参 ReservationChannel 选重载。旧版通用方法直接调用 order.reserve,跳过了整箱数量检查:库存从 3 变成 2,订单记为已预留 1。第二行的实参静态类型就是 Bundle,因此命中窄重载并抛 IllegalArgumentException,库存保留 3。两次调用不能共用同一订单作为证据,因为订单第一次预留后再调用会先触发“重复预留”;实验分别创建新订单和新库存。
sequenceDiagram
participant Caller as 调用方
participant Compiler as 编译期
participant Channel as 运行时 Bundle
participant Order as Order
Caller->>Compiler: reserve(channel: ReservationChannel, qty=1)
Compiler-->>Caller: 选中 reserve(ReservationChannel, ...)
Caller->>Order: Before 直接调用 order.reserve(1, stock)
Note over Channel: 没有调用 Bundle.reserve
Order-->>Caller: 库存减少 1,违背整箱规则
让变化落在被分派的方法上
After.reserve 只保留 ReservationChannel 签名,方法体调用 channel.reserve(order, inventory, quantity)。ReservationChannel.Standard 和 ReservationChannel.Bundle 实现同一个接口方法;编译器选定接口签名后,运行时接收者是 Bundle,才执行其整箱条件检查。检查发生在 Order.reserve 之前,所以奇数被拒时,库存与订单都不变。接口不是用来“消除条件分支”以凑模式:它承担的是根据运行时渠道变化的行为。
测试还单独建立了 Recorder 与 DetailedRecorder:变量静态类型为 Recorder,实际对象为子类。当参数静态类型是 ReservationChannel 时,调用被子类重写的通用签名,得到 override;参数写成 new Bundle() 时,先选父类的 describe(Bundle) 重载,得到 bundle overload。这两个断言同时约束“选哪个签名”和“执行哪个实现”,而不只是比较类名。
| 库存 3 件时的调用 | 旧版 Before |
接口分派 After |
简单分支 Alternative |
|---|---|---|---|
| 普通/整箱各取 2(分别使用新订单) | 都成功,库存各余 1 | 同左 | 同左 |
| 声明为接口的整箱对象取 1 | 误成功,库存余 2 | 拒绝,库存仍 3 | 拒绝,库存仍 3 |
| 整箱取 4;已取 2 再取 2 | 不作为旧版新增契约依据 | 分别拒绝且保持拒绝前状态 | 同左 |
Alternative 没有在调用端加重载,而是在单个方法里用 instanceof Bundle 判断奇数,再调用既有 Order.reserve。两种修正版都保留了 03 的非法数量、库存不足和重复预留的异常类型与状态断言。若渠道只有两个且新增概率低,显式分支更省一层策略协作;若独立渠道规则持续增加,并且调用方只持有接口引用,接口分派更能把变化留在对应实现。实验没有因此修改累计 order-core:普通订单的预留契约本身不需要知道整箱渠道。
验证范围与练习
2026-10-02 用 Ubuntu OpenJDK 21.0.12.1、Maven Wrapper 3.9.9 运行 ./mvnw -B -ntp verify,退出码 0。04 的 9 次测试与 00–03 的 25 次回归均无失败;精确输入、预期违约、环境与原始输出见 examples/design-patterns/evidence/04/。所有库存和订单都是合成输入;这里仅验证同步、单线程的接口调用,不声称支持并发预留或事务回滚。
- 新增“至少取 2 件、允许奇数”的团购渠道。先预测
Before若仅添一个新重载,调用方声明为ReservationChannel时会选哪个签名;再对After和Alternative增加“取 1 拒绝且两份状态不变、取 3 成功、原整箱取 1 仍拒绝”的断言。用可失败的测试验证预测,不靠打印。 - 把
Recorder.describe(Bundle)移到DetailedRecorder中,但只加重载、不重写describe(ReservationChannel)。分别预测Recorder recorder = new DetailedRecorder()上以接口变量、具体Bundle表达式调用的结果;编译并运行后解释静态类型为何限制可见的重载集合。
参考与系列导航
- Java SE 21 JLS §15.12.2.5:编译期重载的最具体方法选择。
- Java SE 21 JLS §15.12.4.4、§8.4.8.1:实例方法的运行期定位及类方法重写。
- 03 设计契约 ← 04 多态与动态分派(本篇) → 05 继承、组合与委托。





