Java密封类与穷尽模式匹配
摘要
一篇探讨Java密封类与穷尽模式匹配的博文,指出添加新的子类型可能会破坏消费者代码,而过期的编译产物会导致运行时MatchException而非编译期错误。作者总结认为,密封类型最好在组件内部使用,而非暴露在公共API中。
暂无内容
查看缓存全文
缓存时间: 2026/10/18 16:56
# Java 密封类与穷尽模式匹配
来源:https://neilmadden.blog/2026/04/24/java-sealed-classes-and-exhaustive-pattern-matching/
Java 17 引入了[密封类](https://openjdk.org/jeps/409),允许你显式列出一个接口或基类的允许子类型。例如,下面是一个使用密封接口和 record 的简单示例(如果未显式列出允许的子类型,内部类会被隐式添加到其中):
```java
public sealed interface SealedType {
record TypeA() implements SealedType {}
record TypeB() implements SealedType {}
static SealedType of(String type) {
return switch (type) {
case "A" -> new TypeA();
case "B" -> new TypeB();
default -> throw new IllegalArgumentException();
};
}
}
```
如果你熟悉具有代数数据类型的函数式编程语言,可以将其类比为 Haskell 或 ML 中的数据类型声明:
```haskell
data SealedType = TypeA | TypeB
```
然后我们可以在一个简单的 Main 类中使用它:
```java
void main(String[] args) {
var val = SealedType.of(args[0]);
System.out.println(switch (val) {
case SealedType.TypeA() -> "A";
case SealedType.TypeB() -> "B";
});
}
```
好的,这看起来没什么特别的。但值得注意的一点是,在 main 方法的 switch 表达式中,我们不需要添加 `default` 子句。这是因为密封类(以及枚举)启用了*穷尽性检查*:编译器精确知道所有可能的情况,因此可以检查你是否覆盖了所有情况。如果你已经全部覆盖,就不需要 `default` 子句。如果你遗漏了一个(且没有 `default` 子句),则会产生编译时错误。
当你想确保某个类型的所有使用都覆盖了所有情况时,这非常好,但它引入了一种新的破坏性变更类型:向密封类/接口添加新的子类型可能会破坏该代码的使用者。例如,向我们的示例中添加一个新的 `TypeC` 情况,会导致 main 方法因缺少匹配分支而无法编译。因此,如果你在 API 中导出了密封类型,那么添加新的子类型就是一个破坏性变更,需要提升主版本号(如果你遵循 SemVer 的话)。
## 编译时错误还是运行时错误?
虽然当你编译消费者代码(本例中是 main 方法)时,Java 会对非穷尽的 switch 产生编译时错误,但如果密封类型发生变化时消费者没有被重新编译,Java 就无法做到这一点。例如,假设我们在 SealedType 中扩展了另一个情况:
```java
public sealed interface SealedType {
record TypeA() implements SealedType {}
record TypeB() implements SealedType {}
record TypeC() implements SealedType {}
static SealedType of(String type) {
return switch (type) {
case "A" -> new TypeA();
case "B" -> new TypeB();
case "C" -> new TypeC();
default -> throw new IllegalArgumentException();
};
}
}
```
如果我们只是重新编译了 SealedType.java 而没有重新编译 Main,那么当我们触发新的情况时,得到的是一个**运行时异常**:
此时抛出的是新的 [MatchException](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/MatchException.html)。Javadoc 中指出了这种独立编译带来的潜在问题,以及模式匹配中关于 null 值的一些边界情况。因此,即使你希望使用密封类能够在添加新情况时静态地确保更新所有使用者,但除非你重新编译所有代码,否则这是做不到的。
## 结论
对我来说,结论是:密封类型可能在组件的实现*内部*最为有用,而在组件向其他组件(如库)公开的公共 API 中则不太有用。对于内部使用场景,你通常会将所有代码一起重新编译,从而获得穷尽性检查和更高的编译时安全保障的好处。但当跨越模块边界使用时,你可能只是引入了新的破坏代码的方式,而且往往只能在运行时才能发现。
(我是在审查 [PEM 编码加密对象的预览支持](https://openjdk.org/jeps/524)时发现了这些细节的,该功能恰好犯了这个错误——将密封接口嵌入到公共 API 中,并建议客户端对该类型进行模式匹配。我预计如果他们将来想添加新的情况,有极高的可能性会造成破坏。)
相似文章
设计模式很烂
本文认为设计模式被高估了,它们常常是对语言局限性不必要的变通,以Java为主要例子,并与Scala等更具表现力的语言进行对比。
在 Java 中实现完整的 Haskell 风格类型类解析
一篇 79 分钟的深度博客文章,演示如何在 Java 中实现完整的 Haskell 风格类型类解析,涵盖类型统一、见证者解析、高阶类型以及 Functor、Applicative 和 Monad 等示例。
Value Classes 仍需编译器的关怀
本文讨论了 Value Classes 作为 JDK 28 中的预览功能的集成,并分析了 JVM 中的优化机会和限制,着重强调了谨慎的编译器支持的必要性。
Gleam 的 case 穷尽性检查——一位粉丝的深入探索
一篇由 Gleam 粉丝撰写的深度文章,深入探讨 Gleam 的 case 穷尽性检查机制,分析 compiler-core/src/exhaustiveness.rs 源码,并将模式匹配的编译理论追溯到 Lennart Augustsson 于 1984 年发表的论文《Compiling Pattern Matching》。
@charliermarsh:ty 现在支持全部22种控制流分析和类型收窄,这些是在这个巨大的"Advanced pattern …"问题中识别出来的。
Charlie Marsh 宣布,'ty' 工具现在支持高级模式匹配中全部22种控制流分析和类型收窄案例。