为什么枚举上的开关需要默认值?

问题描述 投票:34回答:7

通常,switch语句中不需要default。但是,在以下情况下,代码只有在取消注释默认语句时才能成功编译。谁能解释为什么?

public enum XYZ {A,B};
public static String testSwitch(XYZ xyz)
{
    switch(xyz)
    {
    case A:
        return "A";
    case B:
    //default:
        return "B";
    }
}
java enums default switch-statement
7个回答
43
投票

您必须取消注释default的原因是您的函数表示它返回String,但如果您只为caseA定义了B标签,那么如果您传入任何其他内容,该函数将不会返回值。 Java要求所有声明它们返回值的函数实际上在所有可能的控制路径上返回一个值,并且在您的情况下,编译器不相信所有可能的输入都返回了值。

我相信(我不确定)这样做的原因是即使你覆盖了所有的enum案例,代码在某些情况下仍可能失败。特别是,假设您编译包含此switch语句的Java代码(工作正常),然后更改enum以便现在有第三个常量 - 让我们说C - 但是你不用switch重新编译代码声明。现在,如果您尝试编写使用以前编译的类并将C传入此语句的Java代码,那么代码将没有返回值,违反了Java契约,所有函数始终返回值。

从技术上讲,我认为真正的原因是JVM字节码验证器总是拒绝函数,其中有一些控制路径从函数的末尾掉落(参见§4.9.2of the JVM spec),所以如果代码是编译的话它无论如何都会在运行时被JVM拒绝。因此,编译器会向您报告错误,以报告存在问题。


40
投票

我认为这是由switch语句(JLS 16.2.9)的JLS明确赋值规则解释的,其中陈述如下:

“如果满足以下所有条件,则在切换语句后指定V [un]:

  • 交换机块中有一个默认标签,或者在交换机表达式后指定了V [un]。

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

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


编译器必须提供错误的另一个(更合理的)原因。它源于enumJLS 13.4.26)的二进制兼容性规则,其中规定了以下内容:

“从枚举类型中添加或重新排序常量不会破坏与预先存在的二进制文件的兼容性。”

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


在Java 12中,他们引入了包括切换表达式的切换增强功能。对于在编译时和运行时之间更改的枚举,这会遇到同样的问题。根据JEP 354,他们解决这个问题如下:

切换表达式的情况必须是详尽的;对于所有可能的值,必须有匹配的开关标签。 (显然,切换语句不需要详尽无遗。)

在实践中,这通常意味着需要默认条款;但是,对于包含所有已知常量的枚举开关表达式,编译器会插入一个default子句,以指示枚举定义在编译时和运行时之间已更改。依赖于这个隐式默认子句插入可以提供更强大的代码;现在,当重新编​​译代码时,编译器会检查是否显式处理了所有情况。如果开发人员插入了显式默认子句(如今的情况),则可能会隐藏可能的错误。

唯一不明确的是隐式默认子句实际上会做什么。我的猜测是它会引发一个未经检查的异常。 (截至目前,尚未更新JLS for Java 12以描述新的切换表达式。)


7
投票

如前所述,您需要返回一个值,并且编译器不会假定枚举在将来不能更改。例如。您可以创建枚举的另一个版本并使用它而无需重新编译该方法。

注意: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);
}

2
投票

在Java 12中,您可以使用预览切换表达式功能(JEP-325),如下所示:

public static String testSwitch(XYZ xyz) {
    return switch (xyz) {
        case A -> "A";
        case B -> "B";
    };
}

只要你处理switch中的所有枚举值,就不需要默认情况。

注意,要使用预览功能,您必须将--enable-preview --source 12选项传递给javacjava


1
投票

有一个契约,这个方法必须返回一个String,除非它抛出一个Exception。并且每次都不限于xyz的值等于XVZ.AXYZ.B的情况。

这是另一个例子,它是obviuos,代码将运行正确但我们有一个编译时错误,原因相同:

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

你必须添加一个默认语句,这是真的,你必须随时返回一个值。因此,要么在切换块之后添加默认语句或添加return语句。


0
投票

因为编译器无法猜测enum中只有两个值,并强制您从方法返回值。 (但我不知道为什么它无法猜测,也许它有反射的东西)。


0
投票
default: throw new AssertionError();
© www.soinside.com 2019 - 2024. All rights reserved.