何时调用密封层次结构的交换机中的默认情况

When would default case in switch for sealed hierarchy be invoked

提问人:Bond - Java Bond 提问时间:11/15/2023 更新时间:11/18/2023 访问量:106

问:

给定如下层次结构sealed

sealed interface Shape permits Rectangle, Square

record Rectangle() implements Shape

record Square() implements Shape

由于 & 是记录,它本质上使整个层次结构不可扩展,即不再允许使用子类。RectangleSquare

从 JDK 21 开始,通过覆盖所有可能或提供案例来覆盖其余部分,强制 switch 的模式匹配是详尽的。switchcasedefault

因此,在什么情况下,由于涵盖了所有可能的组合,因此在什么情况下会执行这种情况,为什么甚至允许这样做?switchdefault

switch (shape) {
    case Rectangle r -> // do something with r;
    case Square sq -> // do something with sq; 
    case null -> // shape could be null 
    default -> // WHY is this permitted when all possible cases are covered already??
    }

P.S.:密封的层次结构肯定可以进化,但是当这种情况发生时,编译器也会自动标记升级自己。switch

switch-语句 java-21

评论

3赞 user85421 11/15/2023
基本上是因为 Java 语言规范中没有指定不允许 - 为什么会这样?首先,必须询问开发人员/说明者。也许是因为如果语句/表达式与类位于完全不同的编译单元中会发生什么,也就是说,可以在第一个不被触及甚至编译器不知道的情况下进行更改/编译 ||顺便说一句,也是允许的......switchShapeShapei += 0
3赞 Naman 11/16/2023
问得好。为了附庸@user85421指出的内容,JEP-441 声明如下: 同样,穷举性的概念是一种近似值。由于单独编译,接口 I 的新实现有可能在运行时出现,因此在这种情况下,编译器将插入一个抛出的合成默认子句。记录模式(JEP 440)使穷举性的概念变得更加复杂,因为记录模式可以嵌套。因此,穷举性的概念必须反映这种潜在的递归结构。
1赞 Bond - Java Bond 11/16/2023
因此,从本质上讲,事情可以归结为边缘情况,对于这些情况,显式可以充当安全网。感谢您@user85421和@Naman的见解default

答:

3赞 Sweeper 11/16/2023 #1

密封的层次结构肯定可以进化,但当这种情况发生时,编译器也会自动标记交换机以进行自我升级。

事实并非如此。您似乎假设 switch 语句和 records/sealed 接口将始终一起编译。当然,这在大多数实际情况下可能是正确的,但并不总是正确的。您可以编译密封的接口和记录,而无需重新编译 switch 语句。

例如,假设这些类型都位于以名称命名的单独 .java 文件中,因此 Shape.java、Rectangle.java、Square.java,而 switch 语句位于 Main.java 的方法中。main

首先,我编译我拥有的所有 Java 源文件,并为每个 .java 文件生成 .class 文件。

然后,假设我将 Shape.java 更改为:

sealed interface Shape permits Rectangle, Square, Triangle

并添加了一个 Triangle .java 文件。

之后,我将只编译记录和密封接口,而不编译 Main.java。这是可能的,因为它们不依赖于Main.java。

最后,我运行.这将运行 Main.class 文件,该文件不知道新类,因为它是在我添加 .java MainTriangleTriangle

这是执行分支的地方,或者如果你没有分支,这里是抛出 a 的地方。defaultdefaultMatchException

(请注意,该部分仅适用于 switch 表达式和增强的 switch 语句,如问题中的语句。如果没有大小写匹配,非增强型 switch 语句根本不会执行任何操作。MatchException

另请参见Java 语言规范中的 switch 语句的执行switch 表达式的运行时计算

7赞 Brian Goetz 11/18/2023 #2

到目前为止,答案大多是正确的,但故事还有更多。

到目前为止给出的简单答案是正确的,即无论编译时类型检查是否详尽,都可能存在与大小写不匹配的运行时值。默认类是允许的,因为它可能被选中(如果不提供,编译器会给你一个引发 MatchException 的合成类。

在编译时详尽的开关在运行时可能不是真正详尽的,主要有两个原因:单独编译和剩余。

其他答复中已充分处理了单独汇编;新的枚举常量和密封类型的新子类型可以在运行时显示,因为可以重新编译层次结构,而无需重新编译其切换。这通常由编译器静默地交给你(让你声明一个只抛出“can't get here”的子句是没有意义的),但如果你愿意,你可以自己处理它。default

第二个原因是余数,这反映了“足够穷尽”的合理含义和实际的穷尽性并不完全一致,如果我们要求开关真正详尽,那么编程将是非常无趣的。

一个简单的例子是这样的:

Box<Box<String>> bbs = ...
switch (bbs) { 
    case Box(Box(String s)): ...
}

这个开关应该打开吗?事实证明,在运行时有一个可能的值不匹配:。(回想一下,嵌套模式与外部模式匹配,然后使用外部模式的绑定作为内部模式的匹配候选项,并且记录模式不能匹配 null,因为它想要调用组件访问器。Box<Box<String>>Box(null)

我们可以要求详尽无遗,有一个单独的错误处理案例,但没有人愿意这样做,而且对于不那么琐碎的例子,错误处理会压倒有用的案例。因此,Java 做出了务实的选择,即用“人类”术语来定义开关穷举性——对于合理的类来说,代码似乎是详尽的——并允许“愚蠢”的情况由合成默认值处理。(如果你愿意,你仍然可以自由地明确地处理任何愚蠢的案例。此开关被认为是详尽的,但余数为非空。Box(null)

整个概念在模式:穷举性、无条件性和余数中得到了更详细的解释。