1.3 代数数据类型与类型推导:让状态空间可见
Product type 表示“同时拥有这些字段”,sum type 表示“只能是这些变体之一”。它们让业务状态的组合数量显式化。
Product 与 Sum 对应乘法和加法
record Point(int x, int y)若 x 有 X 种值、y 有 Y 种值,且两者可独立选择,这个数对有 X × Y 种组合。字段之间若还有约束,合法状态数可能更少。
PaymentResult = Approved(receipt) | Declined(reason) | Pending(reviewId)不同变体的可能值数量相加。每个变体只携带自己的字段,因此 Approved 不再同时带有拒绝原因。但在允许空引用的语言里,和类型不会自动拒绝 null receipt,这条约束仍需构造函数检查。
Java 可用 sealed hierarchy + record + pattern matching 表达,Rust 用 enum,TypeScript 用 discriminated union。关键不在语法,而在变体封闭且 payload 与状态绑定。
Exhaustive matching 让新增状态显式传播
把下面代码保存为 PaymentDemo.java,用 JDK 21 或更高版本运行 java PaymentDemo.java。示例使用已正式定稿的 record pattern 与 switch 模式匹配,不需要预览开关。
import java.util.Objects;
class PaymentDemo {
sealed interface PaymentResult permits Approved, Declined, Pending {}
record Approved(String receipt) implements PaymentResult {
Approved { Objects.requireNonNull(receipt); }
}
record Declined(String reason) implements PaymentResult {
Declined { Objects.requireNonNull(reason); }
}
record Pending(String reviewId) implements PaymentResult {
Pending { Objects.requireNonNull(reviewId); }
}
static String describe(PaymentResult result) {
Objects.requireNonNull(result);
return switch (result) {
case Approved(var receipt) -> "receipt:" + receipt;
case Declined(var reason) -> "declined:" + reason;
case Pending(var id) -> "pending:" + id;
};
}
public static void main(String[] args) {
System.out.println(describe(new Approved("r-1")));
System.out.println(describe(new Declined("limit")));
System.out.println(describe(new Pending("p-1")));
}
}输出依次是 receipt:r-1、declined:limit、pending:p-1。调用 new Approved(null) 或 describe(null) 都会立即抛出 NullPointerException。空字符串仍能通过这里的构造函数;若业务要求非空白文本,还需增加对应校验。
新增一个实现 PaymentResult 的 Cancelled record,并把它加入 permits 列表,原 switch 就应因不再穷尽而编译失败。统一加 default -> null 会掩盖遗漏。对开放的外部协议,可以显式设计 unknown 变体,保留原始输入并有意识地处理。
Optional/Result 把缺失与失败放进类型
Optional<T> 表达可能没有值,Result<T,E> 这类抽象表达成功或有类型的失败;Java 标准库没有同名的通用 Result 类型。它们不必替代所有异常:参数校验失败、业务拒绝和基础设施故障可能需要不同的恢复策略。
避免 Optional<List<T>> 这类不必要双重空状态,除非“字段未提供”与“提供空列表”确实语义不同。类型应反映业务差异,而不是把所有包装器叠上去。
Type inference 解约束,不读心
编译器从字面量、参数、返回上下文和 bounds 收集相等/子类型约束,再求出满足条件的类型。局部推导减少重复,却不应隐藏公共 API 契约。
emptyList() 在不同上下文可能推导为 List<String> 或 List<Order>若缺乏上下文,编译器按当前语言的默认规则选择类型或报错。复杂链式表达式出错时,给中间值加显式类型,可以缩小发生冲突的约束范围。类型推导能省敲键盘,读心这件事暂时不归它管。
类型系统不能替运行时不变量
Email 若只是 String alias,不能保证格式已验证。Smart constructor 可以控制构造:
parseEmail(untrusted) -> Result<Email, ValidationError>要逐项核对实际构造路径:一些序列化器或 ORM 会绕过普通构造函数,另一些则调用构造函数并保留校验;数据库迁移也可能不经过应用构造函数就修改存储数据。关键不变量要在这些边界验证,必要时在存储层也加约束。
类型系统还无法自动证明余额守恒、权限策略或分布式时序;更强的 refinement/dependent type 可以表达更多性质,也带来证明和工具成本。选择与风险匹配的强度。
建模检查
- 哪些字段组合其实代表互斥状态?
null是缺失、未知、未加载还是失败?- 新增变体后,哪些消费者必须显式更新?
- 外部开放枚举如何处理未来未知值?
- 构造函数是否确保内部值合法,边界是否可能绕过?
- 类型推导是否让公共契约或错误位置变得不清楚?
下一章进入类型背后的运行时:规范中的 heap/frame/reference 与实际 JIT、GC、栈上优化并不是同一层描述。
参考资料
Oracle, JLS: Type Inference
OpenJDK, JEP 441: Pattern Matching for switch
OpenJDK, JEP 440: Record Patterns