【问题标题】:Why doesn't String switch statement support a null case?为什么 String switch 语句不支持 null 情况?
【发布时间】:2013-08-15 23:20:05
【问题描述】:

我只是想知道为什么 Java 7 switch 语句不支持 null 情况而是抛出 NullPointerException?请参阅下面的注释行(示例取自the Java Tutorials article on switch):

{
    String month = null;
    switch (month) {
        case "january":
            monthNumber = 1;
            break;
        case "february":
            monthNumber = 2;
            break;
        case "march":
            monthNumber = 3;
            break;
        //case null:
        default: 
            monthNumber = 0;
            break;
    }

    return monthNumber;
}

这可以避免在每次使用 switch 之前进行空检查的 if 条件。

【问题讨论】:

  • 对此没有决定性的答案,因为我们不是创造这种语言的人。所有的答案都是纯粹的猜想。
  • 尝试打开null 将导致异常。执行if 检查null,然后进入switch 语句。
  • 来自JLS在Java编程语言的设计者的判断中,[如果表达式在运行时计算为null,则抛出NullPointerException]是更好的结果而不是静默跳过整个 switch 语句或选择执行默认标签(如果有)之后的语句(如果有)。
  • @gparyani:回答这个问题。这听起来非常正式和明确。
  • @JeffGohlke:“除非您是做出决定的人,否则无法回答为什么的问题。”...好吧,gparyani 的评论证明并非如此跨度>

标签: java switch-statement language-design


【解决方案1】:

作为 cmets 中的 damryfbfnetsi points outJLS §14.11 有以下注释:

禁止使用null 作为开关标签会阻止人们编写永远无法执行的代码。如果switch 表达式是引用类型,即String 或装箱的原始类型或枚举类型,则如果表达式在运行时计算为null,则会发生运行时错误。 在 Java 编程语言的设计者看来,这比默默地跳过整个switch 语句或选择执行default 标签(如果有)之后的语句(如果有)要好。

(强调我的)

虽然最后一句跳过了使用case null: 的可能性,但它似乎是合理的,并提供了对语言设计者意图的看法。

如果我们更愿意看一下实现细节,this blog post by Christian Hujer 对为什么在开关中不允许使用null 有一些有见地的推测(尽管它以enum 开关而不是String 开关为中心) :

在幕后,switch 语句通常会编译为 tablesswitch 字节码。 switch 的“物理”参数及其案例是 ints。要打开的 int 值是通过调用方法Enum.ordinal() 来确定的。 [...] 序数从零开始。

这意味着,将null 映射到0 并不是一个好主意。第一个枚举值的开关与 null 没有区别。也许从 1 开始计算枚举的序数是个好主意。但是它并没有这样定义,而且这个定义不能改变。

String 切换 are implemented differently 时,enum 切换最先出现,并开创了当引用为 null 时切换引用类型的行为的先例。

【讨论】:

  • 如果将空值处理作为case null: 的一部分,这将是一个很大的改进,如果它是专门为String 实现的。目前所有的String 检查都需要一个空检查,如果我们想正确地做的话,虽然主要是隐含地把字符串常量放在前面,如"123test".equals(value)。现在我们不得不像if (value != null) switch (value) {...那样编写我们的switch语句
  • 重新“将 null 映射到 0 不是一个好主意”:这是一种轻描淡写的说法,因为 "".hashcode() 的值是 0!这意味着空字符串和零长度字符串必须在 switch 语句中被同等对待,这显然是不可行的。
  • 对于枚举,是什么阻止了它们将 null 映射到 -1?
【解决方案2】:

一般null 很难处理;没有null,也许更好的语言可以生存。

您的问题可能会解决

    switch(month==null?"":month)
    {
        ...
        //case "":
        default: 
            monthNumber = 0;

    }

【讨论】:

  • 如果month 是空字符串则不是一个好主意:这会将其视为空字符串。
  • 在许多情况下,将 null 视为空字符串是完全合理的
【解决方案3】:

它并不漂亮,但String.valueOf() 允许您在开关中使用空字符串。如果找到null,则将其转换为"null",否则仅返回您传递给它的相同字符串。如果您不明确处理"null",那么它将转到default。唯一需要注意的是,无法区分字符串 "null" 和实际的 null 变量。

    String month = null;
    switch (String.valueOf(month)) {
        case "january":
            monthNumber = 1;
            break;
        case "february":
            monthNumber = 2;
            break;
        case "march":
            monthNumber = 3;
            break;
        case "null":
            monthNumber = -1;
            break;
        default: 
            monthNumber = 0;
            break;
    }
    return monthNumber;

【讨论】:

  • 我相信在 java 中做这样的事情是一种反模式。
  • @ŁukaszRzeszotarski 这就是我所说的“它不漂亮”的意思
【解决方案4】:

这是试图回答为什么会抛出NullPointerException

下面的 javap 命令的输出表明 case 是根据 switch 参数字符串的哈希码选择的,因此当在空字符串上调用 .hashCode() 时会抛出 NPE。

6: invokevirtual #18                 // Method java/lang/String.hashCode:()I
9: lookupswitch  { // 3
    -1826660246: 44
     -263893086: 56
      103666243: 68
        default: 95
   }

这意味着,根据对Can Java's hashCode produce same value for different strings? 的回答,虽然很少见,但仍有可能匹配两种情况(具有相同哈希码的两个字符串)请参阅下面的示例

    int monthNumber;
    String month = args[0];

    switch (month) {
    case "Ea":
        monthNumber = 1;
        break;
    case "FB":
        monthNumber = 2;
        break;
    // case null:
    default:
        monthNumber = 0;
        break;
    }
    System.out.println(monthNumber);

javap

  10: lookupswitch  { // 1
              2236: 28
           default: 59
      }
  28: aload_3       
  29: ldc           #22                 // String Ea
  31: invokevirtual #24                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  34: ifne          49
  37: aload_3       
  38: ldc           #28                 // String FB
  40: invokevirtual #24                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  43: ifne          54
  46: goto          59 //Default

如您所见,"Ea""FB" 只生成了一个案例,但有两个 if 条件来检查每个案例字符串是否匹配。实现此功能的非常有趣且复杂的方式!

【讨论】:

  • 不过,这可以通过其他方式实现。
  • 我会说这是一个设计错误。
  • 我想知道这是否与为什么字符串散列函数的一些可疑方面没有改变有关:通常代码不应该依赖hashCode 在不同运行时返回相同的值程序,但由于字符串哈希被编译器烘焙到可执行文件中,字符串哈希方法最终成为语言规范的一部分。
【解决方案5】:

长话短说……(希望足够有趣!!!)

Enum 最早是在 Java1.52004 年 9 月)中引入的,bug 请求允许打开字符串 早就提交了(Oct '95)。如果您查看在 2004 年 6 月 发布的关于该错误的评论,它会说 Don't hold your breath. Nothing resembling this is in our plans. 看起来他们推迟了(忽略)这个错误并最终在同年,他们引入了序数从 0 开始的“枚举”,并决定 (missed) 不支持枚举的 null。后来在Java1.7Jul'2011)他们遵循(强制)与 String 相同的哲学(即在生成字节码时,在调用 hashcode() 之前没有执行空检查方法)。

所以我认为这归结为枚举首先出现并以从 0 开始的序数开始实现的事实,因此他们无法在 switch 块中支持 null 值,后来他们决定使用 String 他们决定强制使用相同的理念,即开关块中不允许有空值。

TL;DR 使用 String,他们可以在实现 java 代码到字节码的转换时处理 NPE(由于尝试为 null 生成哈希码),但最终决定不这样做。

参考: TheBUG, JavaVersionHistory, JavaCodeToByteCode, SO

【讨论】:

    【解决方案6】:

    根据 Java 文档:

    开关适用于 byte、short、char 和 int 原始数据 类型。它也适用于枚举类型(在枚举类型中讨论), String 类,以及一些包装某些特定类的特殊类 原始类型:Character、Byte、Short 和 Integer(在 数字和字符串)。

    由于null 没有类型,也不是任何事物的实例,因此它不能与 switch 语句一起使用。

    【讨论】:

    • 然而nullStringCharacterByteShortInteger 引用的有效值。
    • @asteri 7 年后,我想指出 switch 语句不比较实例引用值;它比较实例值。
    【解决方案7】:

    答案很简单,如果您使用带有引用类型(例如装箱的原始类型)的开关,则如果表达式为 null,则会发生运行时错误,因为取消装箱会抛出 NPE。

    所以 case null (这是非法的)无论如何都不能被执行;)

    【讨论】:

    • 不过,这可以用另一种方式实现。
    • 好的@Thilo,比我更聪明的人参与了这个实现。如果您知道可以实现此功能的其他方式,我很想知道这些是什么[我相信还有其他方式],所以请分享...
    • 字符串不是装箱的原始类型,NPE 不会发生,因为有人试图“拆箱”它们。
    • @thilo,其他的实现方式是什么?
    • if (x == null) { // the case: null part }
    【解决方案8】:

    我同意@Paul Bellora 的回答中https://stackoverflow.com/a/18263594/1053496 中富有洞察力的cmets(在引擎盖下......)。

    我从我的经验中找到了另一个原因。

    如果 'case' 可以为 null,这意味着 switch(variable) 为 null,那么只要开发人员提供匹配的 'null' case,那么我们可以认为它很好。但是,如果开发人员没有提供任何匹配的“空”案例,将会发生什么。然后我们必须将它与“默认”情况相匹配,这可能不是开发人员打算在默认情况下处理的情况。因此,将“null”与默认值匹配可能会导致“令人惊讶的行为”。 因此,抛出“NPE”将使开发人员明确处理每种情况。我发现在这种情况下抛出 NPE 非常周到。

    【讨论】:

      【解决方案9】:

      使用 Apache StringUtils 类

      String month = null;
      switch (StringUtils.trimToEmpty(month)) {
          case "xyz":
              monthNumber=1;  
          break;
          default:
             monthNumber=0;
          break;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-11-23
        • 1970-01-01
        • 1970-01-01
        • 2010-09-09
        • 1970-01-01
        相关资源
        最近更新 更多