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

1050

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

有人能解释一下,为什么我不能像Java的switch语句那样做吗?


203
它在SE 7中。请求后16年才出现。http://download.oracle.com/javase/tutorial/java/nutsandbolts/switch.html - angryITguy
88
Sun在他们的评估中是诚实的:“不要抱有期待。” 哈哈,http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=1223179 - raffian
3
我认为这是因为她叹了两次气。他们的回复也有点晚,差不多过了10年。那时她可能正在给孙子孙女准备便当盒。 - Neerkoli
14个回答

1026

Java SE 7已经实现了具有String情况的开关语句,至少在16年后它们被首次请求。没有提供延迟的明确原因,但很可能与性能有关。

JDK 7中的实现

现在,该功能已经在javac 通过一个“去糖化”过程实现;使用case声明中的String常量的干净、高级语法在编译时扩展为遵循模式的更复杂的代码。生成的代码使用了一直存在的JVM指令。

具有String情况的switch在编译期间被转换为两个开关。第一个将每个字符串映射到唯一的整数——它在原始开关中的位置。这是通过首先切换标签的哈希码来完成的。相应的情况是一个测试字符串相等的if语句;如果哈希上有冲突,则测试是一个级联的if-else-if。第二个开关反映了原始源代码中的开关,但用其对应的位置替换了情况标签。这个两步过程使得保留原始开关的流程控制变得容易。

JVM中的开关

更深入了解switch的技术细节,请参考JVM规范,其中描述了switch语句的编译。简而言之,根据case中所使用常量的稀疏程度,有两种不同的JVM指令可用于switch。两者都依赖于为每个case使用整数常量以实现高效执行。
如果常量是密集的,则将它们用作索引(在减去最低值后)到指令指针表中 - tableswitch指令。
如果常量是稀疏的,则执行正确的case的二进制搜索- lookupswitch指令。
在对String对象进行switch时,很可能会同时使用这两个指令。 lookupswitch适用于第一个基于哈希码的switch,以查找case的原始位置。结果的序数非常适合于tableswitch

这两个指令都要求在编译时对分配给每个case的整数常量进行排序。在运行时,虽然tableswitchO(1)性能通常比lookupswitchO(log(n))性能更好,但需要一些分析来确定表是否足够密集以证明空间-时间权衡。Bill Venners撰写了一篇很棒的文章,更详细地介绍了这一点,以及其他Java流程控制指令的底层实现。

JDK 7之前

JDK 7之前,enum可以近似于基于String的switch语句。这使用编译器在每个enum类型上生成的静态的valueOf方法。例如:

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

27
在基于字符串的条件语句中,使用If-Else-If可能比使用哈希表更快。我发现当只存储少量项目时,使用字典会相当昂贵。 - Jonathan Allen
88
if-elseif-elseif-elseif-else 可能更快,但我会在99次中选择更清晰的代码。 字符串是不可变的,它们缓存它们的哈希码,因此"计算"哈希值很快。人们需要对代码进行性能分析以确定其中的好处。 - erickson
23
对于添加 switch(String) 的反对理由是它不符合 switch() 语句的性能保证,他们不想“误导”开发者。说实话,我认为他们本来就不应该保证 switch() 的性能。 - Gili
2
如果您只是使用“Pill”根据“str”执行某些操作,我会认为if-else更可取,因为它允许您处理超出RED、BLUE范围的“str”值,而无需从“valueOf”捕获异常或手动检查与每个枚举类型的名称匹配,这只会增加不必要的开销。在我的经验中,只有在稍后需要字符串值的类型安全表示时,才有意义使用“valueOf”转换为枚举。 - MilesHampson
1
@fernal73 这取决于你有多少个级联的if语句,以及switch字符串的哈希码是否已经计算。对于两个或三个条件,它可能会更快。但是在某些情况下,switch语句可能会表现得更好。更重要的是,对于许多情况,switch语句可能更易读。 - erickson
显示剩余4条评论

129
如果你的代码中有一个可以根据字符串进行切换的地方,那么最好将该字符串重构为可能值的枚举类型,然后可以根据枚举类型进行切换。当然,这会限制您可以拥有的字符串潜在值仅限于枚举中的值,这可能是期望的也可能不是期望的。
当然,您的枚举类型可以有一个条目为“其他”,并具有一个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;
}

4
这种技术还可以让你决定大小写敏感性、别名等问题,而不是依赖语言设计者提出“一刀切”的解决方案。 - Darron
3
同意JeeBee的观点,如果你正在切换字符串,可能需要使用枚举。字符串通常表示将要传递给接口(用户或其他)的某个内容,这个内容在未来可能会更改,因此最好用枚举替换它。 - hhafez
18
请参阅http://www.xefer.com/2006/12/switchonstring以了解这种方法的详细介绍。 - David Schmitt
@DavidSchmitt 这篇文章有一个主要缺陷。它捕获了所有异常,而不是方法实际抛出的异常。 - M. Mimpen

91

以下是一个完整的示例,基于JeeBee的帖子,使用Java枚举代替使用自定义方法。

请注意,在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
  }
}

27
基于整数的switch语句可以被优化为非常高效的代码。基于其他数据类型的switch语句只能编译成一系列if()语句。
因此,C和C++只允许在整数类型上使用switch语句,因为在其他类型上使用没有意义。
C#的设计者认为风格很重要,即使没有优势也要这样做。
Java的设计者显然像C的设计者一样思考。

27
基于任何可哈希对象的开关可以使用哈希表非常高效地实现,例如 .NET。因此,你的理由并不完全正确。 - Konrad Rudolph
3
@Nalandial: 实际上,只要编译器稍微费点功夫,就不会很昂贵,因为当字符串集已知时,生成完美哈希表相当容易(虽然 .NET 不会这样做,也可能不值得这么做)。 - Konrad Rudolph
3
在对字符串进行哈希处理时(由于其不可变性),似乎是解决这个问题的方法,但您必须记住,所有非final对象都可以覆盖它们的哈希函数。这使得在编译时确保switch中的一致性变得困难。 - martinatime
2
你也可以构建一个DFA来匹配字符串(就像正则表达式引擎所做的那样)。可能比哈希更有效率。 - Nate C-K
将docstrings加载到内存中有助于内省。您不仅知道函数名称和签名,还可以获得它们的文档。非常适合交互式或构建IDE。 - Jonathan Baldwin
显示剩余6条评论

20

自1.7以来,直接使用String的示例也可以展示如下:

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;
    }

}

19

詹姆斯·柯伦简洁地说:“基于整数的开关可以优化为非常高效的代码。基于其他数据类型的开关只能编译成一系列if()语句。出于这个原因,C和C ++只允许在整数类型上进行开关操作,因为在其他类型上进行开关操作是没有意义的。”

我的观点就是,一旦你开始在非基本类型上进行开关操作,你就需要开始考虑“equals”与“==”。首先,比较两个字符串可能会是一个相当冗长的过程,这会增加上述性能问题。其次,如果有使用字符串开关,则会要求忽略大小写、考虑/忽略位置环境、基于regex进行开关等。我赞同一项决定,即以节省语言开发者大量时间为代价,为程序员节约一小部分时间。


从技术上讲,正则表达式已经“开关”,因为它们基本上只是状态机;它们仅有两种“情况”,即“匹配”和“不匹配”。(不考虑[命名]组/等等。) - JAB
3
Java编译器从使用字符串对象的开关语句生成的字节码通常比从链接的if-then-else语句生成的字节码更有效率。 - Wim Deblauwe

12
除了以上的好理由,我想补充一点,很多人今天把switch看作是Java过去(回到C语言时代)面向过程的遗留物,认为它已经过时了。
我不完全同意这种观点,我认为在某些情况下switch仍然有其用处,至少因为它的速度很快。而且,它总比我在某些代码中看到过的一系列级联数字else if要好...
但是确实值得看看需要使用switch的情况,并思考是否可以用更面向对象的方式来替换它。例如,在Java 1.5+中使用枚举类型、可能使用HashTable或其他集合类型(有时我会为我们没有像Lua(没有switch)或JavaScript中那样的第一类函数而感到遗憾),甚至使用多态性。

有时我会后悔我们没有将(匿名)函数作为头等公民。这已不再是事实。 - dorukayhan
@dorukayhan 当然可以。但是您是否想添加一条评论,告诉世界如果我们升级到较新的Java版本,就可以获得所有过去十年的答案? :-D - PhiLho

8
如果您没有使用JDK7或更高版本,可以使用hashCode()进行模拟。因为String.hashCode()通常为不同字符串返回不同的值,对于相等字符串始终返回相等的值,所以它是相当可靠的(如@Lii在评论中提到的,不同的字符串可能会产生相同的哈希码,例如"FB""Ea")。请参见文档
因此,代码应该如下所示:
String s = "<Your String>";

switch(s.hashCode()) {
case "Hello".hashCode(): break;
case "Goodbye".hashCode(): break;
}

这样,你就可以在技术上切换一个 int

或者,你可以使用以下代码:

public final class Switch<T> {
    private final HashMap<T, Runnable> cases = new HashMap<T, Runnable>(0);

    public void addCase(T object, Runnable action) {
        this.cases.put(object, action);
    }

    public void SWITCH(T object) {
        for (T t : this.cases.keySet()) {
            if (object.equals(t)) { // This means that the class works with any object!
                this.cases.get(t).run();
                break;
            }
        }
    }
}

5
同一个哈希码可能对应不同的字符串,因此如果使用哈希码来判断不同情况,可能会选择错误的分支。 - Lii
@Lii 感谢您指出这一点!虽然不太可能,但我不会相信它能正常工作。 "FB"和"Ea"具有相同的哈希码,因此找到碰撞并非不可能。第二个代码可能更可靠。 - user4157653
我很惊讶这段代码可以编译通过,因为我以为case语句必须始终是常数值,而String.hashCode()并不是常数值(即使在实践中它的计算结果从未在 JVM 之间改变)。 - StaxMan
@StaxMan 嗯,有趣的是,我从未停下来观察过这一点。但是,是的,case语句的值不必在编译时确定,因此它可以很好地工作。 - hyper-neutrino

4

其他答案说这是在Java 7中添加的,并提供了早期版本的解决方法。本答案尝试回答“为什么”

Java是对C++过于复杂的反应,它被设计成一种简单清晰的语言。

String在语言中得到了一些特殊处理,但我认为设计者们试图将特殊情况和语法糖的数量保持最少。

在底层上,switch语句中的字符串切换相当复杂,因为字符串不是简单的原始类型。在Java设计时,这不是一个常见功能,也与极简主义的设计格格不入。特别是因为他们决定不为字符串特例化==,如果case在这里起作用,那么(现在)会有些奇怪。

在1.0和1.4之间,语言本身基本保持不变。 Java的大部分增强功能都是在库方面进行的。

这一切都随着Java 5的到来而改变,语言得到了大幅度扩展。在版本7和8中,还有进一步的扩展。我预计这种态度的改变是由于C#的崛起所驱动的。


关于 switch(String) 如何与历史、时间线和上下文的 cpp/cs 相适应的叙述。 - Espresso
没有实现这个功能是一个严重的错误,其他的都是便宜的借口。由于缺乏进展和设计师不愿意让语言发展,Java在这些年失去了很多用户。幸运的是,在JDK7之后,他们彻底改变了方向和态度。 - firephil

4
多年来,我们一直在使用一个(开源的)预处理器来完成这个任务。
//#switch(target)
case "foo": code;
//#end

预处理文件名为Foo.jpp,通过一个ant脚本处理成Foo.java。
优点是它被转换成可以在1.0上运行的Java(尽管通常我们只支持到1.4)。相比于使用枚举或其他变通方法,这样做更容易(有很多字符串切换)-代码更容易阅读、维护和理解。据我所知(此时无法提供统计数据或技术原理),它也比自然的Java等效代码更快。
缺点是你不能编辑Java,所以工作流程会有些复杂(编辑、处理、编译/测试),而且IDE将链接回Java,这有点复杂(开关变成了一系列if/else逻辑步骤),并且开关案例顺序没有保持不变。
我不建议在1.7+中使用它,但如果你想编写针对早期JVM的Java程序(因为普通用户很少安装最新版本),它是有用的。
你可以通过从SVN获取在线浏览代码来获得它。您需要EBuild才能按原样构建它。

6
您不需要1.7 JVM即可运行带有字符串开关的代码。1.7编译器将字符串开关转换为使用先前存在的字节码的东西。 - Dawood ibn Kareem

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