题 为什么我不能在String上使用switch语句?


这个功能是否会被放入以后的Java版本中?

有人可以解释为什么我不能这样做,就像Java的技术方式一样 switch 声明有效吗?


931
2017-12-03 18:23


起源


它是在SE 7. 16年后的要求。 download.oracle.com/javase/tutorial/java/nutsandbolts/... - giulio
Sun在他们的评估中是诚实的: "Don't hold your breath."大声笑, bugs.sun.com/bugdatabase/view_bug.do?bug_id=1223179 - raffian
@raffian我认为这是因为她感叹了两次。在将近10年后,他们回复的时间有点晚了。那时她可能正在给她的孙子们打包饭盒。 - WeirdElfB0y


答案:


切换语句 String 案件已经实施 Java SE 7,至少16年 在他们第一次被要求之后。 没有提供延迟的明显原因,但可能与性能有关。

在JDK 7中实现

该功能现已实施 javac  用“去糖”过程; 一个干净的高级语法使用 String 常数 case 声明在编译时扩展为模式后面的更复杂的代码。生成的代码使用始终存在的JVM指令。

一个 switch 同 String 在编译期间,case被转换为两个开关。第一个将每个字符串映射到一个唯一的整数 - 它在原始开关中的位置。这是通过首先打开标签的哈希码来完成的。相应的案例是 if 测试字符串相等的语句;如果哈希上有冲突,则测试是级联的 if-else-if。第二个开关镜像原始源代码中的那个,但用它们对应的位置替换案例标签。这个两步过程可以很容易地保留原始开关的流量控制。

在JVM中切换

有关更多技术深度 switch,你可以参考JVM规范,其中 汇编switch语句 被描述。简而言之,有两种不同的JVM指令可用于交换机,具体取决于案例使用的常量的稀疏性。两者都依赖于使用整数常量来有效执行每种情况。

如果常量是密集的,它们将被用作索引(在减去最低值之后)到指令指针表中 - tableswitch 指令。

如果常量是稀疏的,则执行二进制搜索正确的情况 - lookupswitch 指令。

在脱糖a switch 上 String 对象,可能都使用这两个指令。该 lookupswitch 适用于第一次切换哈希码以查找案例的原始位置。由此产生的序数非常适合a tableswitch

两个指令都要求在编译时对分配给每个案例的整数常量进行排序。在运行时,而在 O(1) 的表演 tableswitch 一般看起来比 O(log(n)) 的表演 lookupswitch,它需要一些分析来确定表是否足够密集以证明时空权衡。比尔维纳斯写道 一篇好文章 更详细地介绍了这一点,以及其他Java流程控制指令的内幕。

在JDK 7之前

在JDK 7之前, enum 可以估计一个 String基于开关。这用了 静电 valueOf 每个编译器生成的方法 enum类型。例如:

Pill p = Pill.valueOf(str);
switch(p) {
  case RED:  pop();  break;
  case BLUE: push(); break;
}

962
2017-12-03 18:30



使用If-Else-If而不是基于字符串的交换的哈希可能更快。我发现只存储一些物品时字典非常昂贵。 - Jonathan Allen
if-elseif-elseif-elseif-else可能会更快,但我会从100个中删除更清晰的代码99次。字符串是不可变的,缓存它们的哈希码,因此“计算”哈希很快。人们必须分析代码以确定有什么好处。 - erickson
添加switch(String)的原因是它不符合switch()语句所期望的性能保证。他们不想“误导”开发者。坦率地说,我不认为他们应该保证switch()的性能开始。 - Gili
如果你刚刚使用 Pill 采取一些行动为基础 str 我认为if-else更可取,因为它允许你处理 str RED,BLUE范围之外的值,无需捕获异常 valueOf 或者手动检查每个枚举类型名称的匹配,这只会增加不必要的开销。根据我的经验,使用它只是有意义的 valueOf 如果稍后需要String值的类型安全表示,则转换为枚举。 - MilesHampson
我想知道编译器是否会努力测试是否存在任何一组数值(x,y) (hash >> x) & ((1<<y)-1) 会为每个字符串产生不同的值 hashCode 是不同的,和 (1<<y) 字符串的数量少于两倍(或至少不大于字符串数)。 - supercat


如果您在代码中有一个可以打开String的位置,那么最好将String重构为可能值的枚举,您可以打开它。当然,您可以将字符串的潜在值限制为枚举中的字符串,这可能是也可能不是。

当然你的枚举可能有一个'other'的条目,还有一个fromString(String)方法,那么你可以拥有

ValueEnum enumval = ValueEnum.fromString(myString);
switch (enumval) {
   case MILK: lap(); break;
   case WATER: sip(); break;
   case BEER: quaff(); break;
   case OTHER: 
   default: dance(); break;
}

120
2017-12-03 18:44



这种技术还可以让您决定不区分大小写,别名等问题。而不是依赖语言设计师来提出“一刀切”的解决方案。 - Darron
同意JeeBee,如果你打开字符串可能需要一个枚举。该字符串通常表示某个接口(用户或其他)可能会在未来发生变化,因此最好将其替换为枚举 - hhafez
看到 xefer.com/2006/12/switchonstring 这个方法的一个很好的写作。 - David Schmitt
@DavidSchmitt这篇文章有一个主要缺陷。它抓住了 所有 异常而不是方法实际抛出的异常。 - M. Mimpen


以下是基于JeeBee帖子的完整示例,使用java enum而不是使用自定义方法。

请注意,在Java SE 7及更高版本中,您可以在switch语句的表达式中使用String对象。

public class Main {

    /**
    * @param args the command line arguments
    */
    public static void main(String[] args) {

      String current = args[0];
      Days currentDay = Days.valueOf(current.toUpperCase());

      switch (currentDay) {
          case MONDAY:
          case TUESDAY:
          case WEDNESDAY:
              System.out.println("boring");
              break;
          case THURSDAY:
              System.out.println("getting better");
          case FRIDAY:
          case SATURDAY:
          case SUNDAY:
              System.out.println("much better");
              break;

      }
  }

  public enum Days {

    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
  }
}

90
2017-09-16 13:11





基于整数的开关可以针对非常高效的代码进行优化。基于其他数据类型的开关只能编译为一系列if()语句。

因此,C&C ++只允许对整数类型进行切换,因为它对其他类型没有意义。

C#的设计师认为风格很重要,即使没有优势。

Java的设计者显然像C的设计师一样。


26
2017-12-03 18:32



可以使用哈希表非常有效地实现基于任何可哈希对象的开关 - 请参阅.NET。所以你的理由并不完全正确。 - Konrad Rudolph
是的,这是我不明白的事情。从长远来看,他们是否担心散列物体会变得太贵? - Alex Beardsley
@Nalandial:实际上,通过编译器的一点努力,它并不昂贵,因为当知道字符串集时,生成完美的散列非常容易(但这不是由.NET完成的;也许不值得努力。 - Konrad Rudolph
@Nalandial和@Konrad Rudolph - 虽然哈希一个字符串(由于它是不可变的性质)似乎是这个问题的解决方案,你必须记住所有非最终对象都可以覆盖它们的哈希函数。这使得在编译时难以确保交换机的一致性。 - martinatime
您还可以构造一个DFA来匹配字符串(就像正则表达式引擎一样)。可能比散列更有效。 - Nate C-K


James Curran简洁地说:“基于整数的开关可以优化为非常有效的代码。基于其他数据类型的开关只能编译为一系列if()语句。因此,C&C ++只允许在整数类型上切换,因为它与其他类型毫无意义。“

我认为,只有这样,一旦你开始转换非原语,你需要开始考虑“等于”与“==”。首先比较两个字符串可能是一个相当冗长的过程,增加了上面提到的性能问题。其次,如果有字符串切换,将需要切换字符串忽略大小写,切换字符串考虑/忽略语言环境,根据正则表达式切换字符串....我会赞成一个为节省大量时间的决定语言开发人员花费了少量时间为程序员。


18
2017-12-03 20:49



从技术上讲,正则表达式已经“切换”,因为它们基本上只是状态机;他们只有两个“案例”, matched 和 not matched。 (不考虑像[named] groups / etc这样的东西。) - JAB
docs.oracle.com/javase/7/docs/technotes/guides/language/... 状态: Java编译器通常使用String对象生成比使用链式if-then-else语句更高效的字节码。 - Wim Deblauwe


直接的一个例子 String 自1.7以来的使用情况也可能会显示:

public static void main(String[] args) {

    switch (args[0]) {
        case "Monday":
        case "Tuesday":
        case "Wednesday":
            System.out.println("boring");
            break;
        case "Thursday":
            System.out.println("getting better");
        case "Friday":
        case "Saturday":
        case "Sunday":
            System.out.println("much better");
            break;
    }

}

18
2018-04-09 06:30





除了上述好的论点,我还要补充一点,今天很多人都看到了 switch 作为Java的过程性过去的剩余部分(回到C次)。

我想,我并不完全赞同这种观点 switch 在某些情况下可以有用,至少因为它的速度,无论如何它比一些级联数值更好 else if 我在一些代码中看到了......

但实际上,值得研究一下你需要一个开关的情况,并看看它是否不能被更多的OO取代。例如Java 1.5+中的枚举,也许是HashTable或其他一些集合(有时我很遗憾,我们没有(匿名)函数作为一等公民,如Lua - 没有switch - 或JavaScript)甚至多态。


12
2017-12-03 21:45



“有时我很遗憾我们没有(匿名)作为头等公民的职能” 那不再是真的。 - dorukayhan
@dorukayhan是的,当然。但是你想在过去十年的所有答案中添加评论,告诉全世界如果我们更新到Java的新版本,我们可以拥有它们吗? :-D - PhiLho