为什么枚举类型中的开关需要默认值?

58

通常在 switch 语句中不需要 default。但是,在下面这种情况下,只有当我取消注释 default 语句时,代码才能成功编译。有人能解释一下为什么吗?

public enum XYZ {A,B};
public static String testSwitch(XYZ xyz)
{
    switch(xyz)
    {
    case A:
        return "A";
    case B:
    //default:
        return "B";
    }
}
8个回答

58
你需要取消注释 default 的原因是,你的函数声明返回一个 String,但如果你只为 AB 定义了 case 标签,则如果传入其他任何值,函数将不会返回任何值。Java 要求所有声明返回值的函数在所有可能的控制路径上实际返回值,在你的情况下,编译器并不确信所有可能的输入都有返回值。
我相信(但不确定)这样做的原因是,即使覆盖了所有的 enum 情况,代码仍然可能在某些情况下失败。特别是,假设你编译包含此 switch 语句的 Java 代码(它可以正常工作),然后稍后更改 enum,使其现在有第三个常量 - 比如说 C - 但你没有重新编译带有 switch 语句的代码。现在,如果你尝试编写使用先前编译的类并将 C 传递到该语句中的 Java 代码,则代码将无法返回值,违反了 Java 所有函数始终返回值的约定。
更加技术性地说,我认为真正的原因是 JVM 字节码验证器始终会拒绝存在某些控制路径在函数结尾处没有结束的函数(请参见JVM规范的 §4.9.2),因此如果代码编译通过,在运行时 JVM 将会拒绝它。编译器因此会给出错误以报告问题存在。

2
@apoorv020,我不认为这是愚蠢的。因为这种优化显然是有原因的。 - Alex Nikolaenkov
3
@apoorv020- 我刚刚用一个相当合理的理由更新了我的答案,说明编译器为什么会拒绝这个。一般来说,编译器并不愚蠢——有一些非常聪明的人倾注心血使它们变得更加强大——所以在编译器允许的设计决策中,可能有许多原因。 - templatetypedef
2
@templatetypedef,你处理这种“默认”情况的方法是什么?我个人从不提供“default”,并在这种方法的结尾抛出一个“IllegalArgumentException”。 - Alex Nikolaenkov
4
编译器并不是傻的。它将此视为错误实际上是因为《Java语言规范》要求如此。请参见我的答案以获取解释。 - Stephen C
3
@EasterBunnyBugSmasher,自从我上次进行重要的Java编程以来,情况可能已经发生了变化,但是在旧版本中如果您在null枚举上执行switch,则会触发NullPointerException而不是考虑任何case标签。如果仍然是这种情况,那么OP的代码将触发异常而不是到达函数结尾而没有返回。 - templatetypedef
显示剩余4条评论

52

我认为这可以通过JLS 16.2.9中对switch语句的明确赋值规则来解释,其规则如下:

"如果满足以下所有条件,则在switch语句之后V是[未]分配的:

  • 在switch块中有一个默认标签或者在switch表达式之后V是[未]分配的。

如果我们将此应用于虚构的方法返回值V,我们可以看到,如果没有default分支,则该值在概念上未被分配。

好的... 我正在推断明确赋值规则以涵盖返回值,也许它们不是。但是我在规范中找不到更直接的东西并不意味着它不存在 :-)


还有一个(更加严谨的)原因,为什么编译器必须报错。这源于JLS 13.4.26中枚举类型的二进制兼容性规则,其规定如下:

"向枚举类型添加或重新排序常量不会破坏与现有二进制文件的兼容性。"

那么在这种情况下如何应用呢?假设编译器被允许推断OP示例switch语句总是返回某个值。如果程序员现在更改枚举以添加额外的常量,会发生什么?根据JLS二进制兼容性规则,我们没有破坏二进制兼容性。然而,包含switch语句的方法现在可以(根据其参数)返回未定义的值。这是不能被允许的,因此switch 必须成为编译错误。


在Java 12中,他们引入了增强的switch语句,其中包括switch表达式。这与在编译时和运行时之间更改的枚举相同。根据JEP 354,他们解决了此问题如下:

switch表达式的case必须是全面的;对于所有可能的值,都必须有匹配的switch标签。(显然,switch语句不需要是全面的。)

实际上,这通常意味着需要一个默认子句;但是,在涵盖所有已知常量的枚举switch表达式的情况下,编译器会插入一个默认子句,以指示枚举定义在编译时和运行时之间已更改。依赖于这种隐式的默认子句插入可以使代码更加健壮;现在,当代码重新编译时,编译器会检查是否显式处理了所有情况。如果开发人员插入了显式的默认子句(如今天的情况),可能会隐藏一个错误。

唯一不太清楚的是隐式默认子句实际上会做什么。我猜它会抛出一个未经检查的异常。(截至目前,Java 12的JLS尚未更新以描述新的switch表达式。)

9
有趣。因此,本质上这是规范上的缺陷,而不是编译器的问题。由于这个细节,一个很容易在编译时就能发现的错误(在 switch 语句中遗漏枚举值),变成了运行时错误。 - augurar
3
@augurar - 我不明白为什么你认为这是规范的缺陷。Java 的本质就是允许不同的类在不同时刻进行编译。如果想要避免枚举和 switch 之间的(假设的)二进制兼容性问题,唯一的方法就是强制依赖的类在被依赖的类之后重新编译。这将是一个重大的、破坏性的变更……并且是无法接受的。 - Stephen C
1
我同意@aurugar的观点:添加枚举常量通常会破坏行为,因此使其难以在编译时捕获感觉像是规范中的疏忽。顺便说一句,https://dev59.com/EGQn5IYBdhLWcg3we3LD上的建议提供了一个合理的解决方法(如果有点冗长)。在枚举中定义一个抽象方法`visit(EnumInterface)`,调用一个新的具有与枚举情况相对应的方法的`EnumInterface`。当您向枚举添加常量时,自然会向`EnumInterface`添加一个方法。 - Partly Cloudy
1
@PartlyCloudy - 嗯,没错。但反过来说,你会强制Java具有更不灵活的枚举类二进制兼容性规则。枚举值的添加将需要破坏二进制兼容性...这将使枚举类变得没有那么有用。 - Stephen C
4
如果有人向枚举类型添加更多的常量,基于以前版本的代码即使有“default”分支也可能仍然失败。现在我们有了新的常量,一些旧的情况可能需要以新方式处理。因此,我认为这不是一个合适的理由。当然,您也不能提供比抛出错误更合理的“default”情况,因为如果您不知道某些内容是什么,就无法正确处理它们。 - Maksim Gumerov
3
现在你正在谈论应用语义而不是编程语言的语义。 有许多情况下,从应用程序的角度来看,即使面对新的枚举值,default情况不是应用程序错误也是有意义的。 - Stephen C

14
在Java 12中,您可以使用预览开关表达式功能(JEP-325)如下:
public static String testSwitch(XYZ xyz) {
    return switch (xyz) {
        case A -> "A";
        case B -> "B";
    };
}

在switch中处理所有枚举值,就不需要默认情况。请注意,要使用预览功能,您需要将--enable-preview --source 12选项传递给javac和java。

哎?如果你修改XYZ以添加(比如说)一个C值,而你没有修改/重新编译这个方法,那么它会返回什么?试一下…… - Stephen C
1
@StephenC,我会得到编译错误,这就是为什么我写了“只要您处理所有枚举值”的原因。 - Adrian
如果您不重新编译,就无法获得编译错误。我所说的是当您修改枚举但不重新编译包含switch语句的方法时会发生什么情况,因为(现在)该语句未涵盖所有枚举值。 - Stephen C
对于阅读此线程的任何人:请查看@StephenC在他自己的答案中的补充说明。 - David Moles
与switch语句相关的内容。Switch表达式的处理方式略有不同。最新版本的switch表达式JEP似乎表明会插入一个隐式默认值。不清楚默认分支的作用是什么。我认为它会抛出运行时异常... - Stephen C

8
正如所述,您需要返回一个值,编译器并不假设枚举在未来不会发生变化。例如,您可以创建枚举的另一个版本,并在不重新编译该方法的情况下使用它。
注意:对于“xyz”有第三个值为null。
public static String testSwitch(XYZ xyz) {
    if(xyz == null) return "null";
    switch(xyz){
    case A:
        return "A";
    case B:
        return "B";
    }
    return xyz.getName();
}

这与以下语句的结果相同

public static String testSwitch(XYZ xyz) {
     return "" + xyz;
}

唯一避免返回的方法是抛出异常。
public static String testSwitch(XYZ xyz) {
    switch(xyz){
    case A:
        return "A";
    case B:
        return "B";
    }
    throw new AssertionError("Unknown XYZ "+xyz);
}

null值在switch(xyz)上已经抛出异常,这里不需要特殊的返回。请参见JLS 14.11 - Paŭlo Ebermann
@Paulo,除非你不想抛出一个空指针异常,否则它是null。例如,这样做与String.valueOf(x)所做的相同,也不会抛出异常。 - Peter Lawrey

1

有一个合同规定这个方法必须返回一个字符串,除非它抛出异常。而且每次都不仅限于xyz的值等于XVZ.AXYZ.B的情况。

这里有另一个例子,很明显代码会运行正确,但由于同样的原因我们有一个编译时错误:

public boolean getTrue() {
  if (1 == 1) return true;
}

并不是说你一定要添加一个默认语句,而是在任何时候都必须返回一个值。因此,您可以在 switch 块后添加默认语句或返回语句。


0

在您的代码示例中,如果xyz为空,会发生什么?在这种情况下,该方法缺少返回语句。


1
发生的情况是,您会得到一个NullPointerException,它追溯到switch(xyz)语句。 - Nicolas Favre-Felix
@NicolasFavre-Felix:不会。调用testSwitch(null)不会抛出NullPointerException异常。这正是被接受的答案所缺失的:在xyz为null的情况下必须有一个返回语句。 - EasterBunnyBugSmasher
我不确定你的意思,但是它确实会引发 NullPointerException这里有一个例子,你可以自己尝试一下。它执行了一个 switch(null),这就会触发一个 NullPointerException。你还可以参考Oracle官方网站关于switch的教程,上面明确指出:确保switch语句中的表达式不为null,以防止引发NullPointerException - Nicolas Favre-Felix

-1
default: throw new AssertionError();

2
你在哪里回答了他的问题? - J. Doe

-1

因为编译器无法猜测enum中只有两个值,并强制你从方法中返回值。(但我不知道为什么它无法猜测,可能与反射有关)。


是的,我也不知道为什么编译器不能理解XYZ只有两个元素。既然它将XYZ识别为一种类型,显然已经到达了代码的某个部分,并且应该能够注册只存在两个可能元素的事实。 - Ken Wayne VanderLinde
@rlibby:我记得测试过null,看看它是否有所不同,但我认为没有。但我会再次检查。 - apoorv020
我认为你不需要检查空值,因为有一个隐式调用ordinal,但我不确定这是否正确。 - templatetypedef
@rlibby:添加null会产生另一个错误(需要常量表达式)。然而,在switch语句中,实际上使用null调用函数会生成空指针异常。 - apoorv020
1
@rlibby,在switch语句中使用null会导致NPE,因此此控制路径无效。 - Alex Nikolaenkov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接