为什么不能在字符串上使用switch语句?


1004

此功能是否将在以后的Java版本中使用?

有人可以解释为什么我不能这样做吗,就像Java switch语句的技术方法那样?



81
Sun对他们的评估很诚实:"Don't hold your breath."哈哈,bugs.sun.com
bugdatabase / view_bug.do?bug_id =

3
@raffian我想是因为她“叹了口气”两次。在将近10年之后,他们也迟到了一点答复。那时她可能已经在给孙子们打包午餐盒了。
WeirdElfB0y

Answers:


1003

带有String案例的switch语句已在Java SE 7中实现,至少在首次提出要求后的 16年没有提供延迟的明确原因,但可能与性能有关。

在JDK 7中实现

现在,该功能已javac 通过“脱糖”过程实现。Stringcase声明时使用常量的干净,高级语法在编译时扩展为遵循模式的更复杂的代码。生成的代码使用始终存在的JVM指令。

switch带有Stringcase的A 在编译期间转换为两个开关。第一个将每个字符串映射到一个唯一的整数-它在原始开关中的位置。这是通过首先打开标签的哈希码来完成的。相应的情况是if测试字符串是否相等的语句;如果哈希上有冲突,则测试为级联if-else-if。第二个开关在原始源代码中进行镜像,但是用相应的位置替换了大小写标签。此两步过程使保留原始交换机的流量控制变得容易。

在JVM中切换

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

如果常量密集,则将它们用作指令指针表(指令)的索引(减去最小值后)tableswitch

如果常量稀疏,则对lookupswitch指令的正确大小写进行二进制搜索。

switchString物体进行除糖时,可能会同时使用这两种指令。在lookupswitch对散列码的第一开关以找到的情况下的原始位置是合适的。由此产生的序数是自然适合tableswitch

两条指令都要求在编译时对分配给每种情况的整数常量进行排序。在运行时,虽然O(1)性能tableswitch一般显得比更好O(log(n))的性能lookupswitch,它需要一些分析,以确定该表是否是密集足以证明时空权衡。Bill Venners写了一篇很棒的文章,其中更详细地介绍了此内容,同时还深入介绍了其他Java流程控制指令。

在JDK 7之前

在JDK 7之前,enum可以近似String基于的开关。这将使用编译器针​​对每种类型生成的静态valueOf方法enum。例如:

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

26
对于基于字符串的开关,仅使用If-Else-If代替散列可能会更快。我发现仅存储少量项目的字典就非常昂贵。
乔纳森·艾伦,

84
if-elseif-elseif-elseif-else可能会更快,但我会将整洁的代码从100中提取99次。不可变的字符串会缓存其哈希码,因此“计算”哈希值很快。人们将不得不分析代码以确定有什么好处。
erickson

21
拒绝添加switch(String)的原因是,它不符合switch()语句所期望的性能保证。他们不想“误导”开发人员。坦白地说,我认为它们不应该保证switch()的性能。
吉利

2
如果您只是Pill基于某种方式采取行动,str我认为if-else是更可取的,因为它允许您处理strRED,BLUE范围之外的值,而无需捕获异常valueOf或手动检查与名称的匹配每个枚举类型只会增加不必要的开销。以我的经验,只有在valueOf以后需要String值的类型安全表示形式时,才可以使用将其转换为枚举。
MilesHampson

我想知道编译器是否要努力测试是否存在任何一对数字(x,y),因此(hash >> x) & ((1<<y)-1)对于每个hashCode不同的字符串,其值集将产生不同的值,并且(1<<y)少于字符串数的两倍(或等于)至少不超过该值)。
2013年

125

如果您在代码中有一个可以打开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;
}

4
这种技术还可以让您决定是否区分大小写,别名等问题。无需依靠语言设计者来提出“一种尺寸就适合所有人”的解决方案。
达伦(Darron)

2
同意JeeBee,如果您要打开字符串,则可能需要一个enum。该字符串通常表示将来要更改的某个内容(将来会更改或不更改),因此最好将它替换为枚举
hhafez

18
有关此方法的详细介绍,请参见xefer.com/2006/12/switchonstring
David Schmitt'4

@DavidSchmitt本文有一个主要缺陷。它捕获所有异常,而不是方法实际抛出的异常。
M. Mimpen'2

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

26

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

因此,C&C ++仅允许在整数类型上进行切换,因为它与其他类型无关。

C#的设计师认为即使没有优势,样式也很重要。

Java的设计师显然像C的设计师那样思考。


26
使用哈希表可以非常有效地实现基于任何可哈希对象的开关-请参见.NET。因此,您的原因并不完全正确。
康拉德·鲁道夫

是的,这是我不明白的事情。他们是否担心从长远来看哈希对象会变得太昂贵?
Alex Beardsley

3
@Nalandial:实际上,在编译器上稍作努力,它根本就不昂贵,因为当知道字符串集时,很容易生成一个完美的哈希值(不过,.NET并没有做到这一点;可能也不值得。)
康拉德·鲁道夫

3
@Nalandial&@Konrad Rudolph-尽管对字符串进行哈希处理(由于其不可变的性质)似乎是解决此问题的方法,但您必须记住,所有非最终对象都可以覆盖其哈希函数。这使得在编译时难以确保切换的一致性。
martinatime

2
您也可以构造DFA来匹配字符串(就像正则表达式引擎一样)。甚至比散列更有效率。
Nate CK

19

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

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

我的观点(仅此而已)是,一旦您开始启用非基本体,就需要开始考虑“等于”与“ ==”。首先,比较两个字符串可能是一个相当漫长的过程,从而增加了上面提到的性能问题。其次,如果需要打开字符串,则需要忽略大小写的字符串,考虑/忽略语言环境的字符串,基于正则表达式的字符串....我赞成这样的决定,该决定可以节省很多时间:语言开发人员会以少量的时间为程序员付出代价。


从技术上讲,正则表达式已经“切换”了,因为它们基本上只是状态机。他们只有两个“案例”,matchednot matched。(不过,没有考虑到[named] groups / etc之类的东西。)
JAB 2012年

1
docs.oracle.com/javase/7/docs/technotes/guides/language/…指出:与使用链式if-then-else语句相比,Java编译器通常从使用String对象的switch语句生成更有效的字节码。
Wim Deblauwe

12

除了上述良好的论据外,我switch还要补充指出的是,今天很多人都将其视为Java程序过时的一部分(回溯至C时代)。

我并不完全同意这种观点,我认为switch在某些情况下可以发挥它的作用,至少是因为它的速度,无论如何,它比else if我在某些代码中看到的一系列级联数字更好。

但是确实,值得一看的是您需要一个开关的情况,看看是否不能用更多的OO代替它。例如Java 1.5+中的枚举,也许是HashTable或其他一些集合(有时我很遗憾,我们没有作为一流公民的(匿名)函数,如Lua(没有开关)或JavaScript)或什至是多态。


“有时候我很遗憾,我们没有作为头等公民的(匿名)职能”,这不再是事实。
user8397947

@dorukayhan是的,当然。但是,您是否要在过去十年的所有答案中添加评论,以告诉世界,如果我们更新到Java的较新版本,我们可以使用它们?:-D
PhiLho

8

如果您没有使用JDK7或更高版本,则可以使用hashCode()它进行仿真。因为String.hashCode()通常为不同的字符串返回不同的值,并且对于相等的字符串总是返回相等的值,所以它是相当可靠的(不同的字符串可以产生与注释中提到的@Lii相同的哈希码,例如"FB""Ea"),请参见documentation

因此,代码如下所示:

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”具有相同的哈希码,因此并非不可能找到冲突。第二个代码可能更可靠。
HyperNeutrino '16

我很惊讶这种编译,因为case我认为语句必须始终是常数,而String.hashCode()并非如此(即使实际上JVM之间的计算从未改变过)。
StaxMan '18

@StaxMan Hm很有趣,我从未停止观察。但是,是的,case语句值不必在编译时就可以确定,因此可以很好地工作。
HyperNeutrino

4

多年来,我们一直在使用(n个开源)预处理器。

//#switch(target)
case "foo": code;
//#end

预处理后的文件名为Foo.jpp,并使用ant脚本处理为Foo.java。

优点是将其处理为可在1.0上运行的Java(尽管通常我们仅支持1.4)。与使用枚举或其他变通方法进行伪造相比,执行此操作(许多字符串切换)要容易得多-代码更易于阅读,维护和理解。IIRC(目前无法提供统计信息或技术推理)也比自然的Java等效更快。

缺点是您不编辑Java,因此工作流程(编辑,处理,编译/测试)要多一些,并且IDE会链接回Java,这有点麻烦(开关变成一系列if / else逻辑步骤)并且不保持开关箱顺序。

我不建议在1.7+上使用它,但是如果您要编写针对较早JVM的Java(因为Joe public很少安装最新版本的Java),它就很有用。

您可以从SVN获得它也可以在线浏览代码。您将需要EBuild才能按原样构建它。


6
您不需要1.7 JVM即可通过String开关运行代码。1.7编译器将String开关转换为使用以前存在的字节码的东西。
达伍德·伊本·卡里姆

4

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

Java是对C ++过于复杂的反应。它被设计为一种简单的简洁语言。

String在语言中有一些特殊情况处理,但对我来说似乎很清楚,设计师正在尝试将特殊大小写和语法糖的数量保持在最低水平。

由于字符串不是简单的原始类型,因此打开字符串非常复杂。在设计Java时,这并不是一个普遍的功能,并且实际上与极简主义设计并不十分吻合。特别是因为他们决定对字符串不使用特殊情况==,所以对于==不适用的情况下的大小写工作会有些奇怪。

在1.0和1.4之间,语言本身几乎保持不变。Java的大多数增强功能都在库方面。

Java 5改变了一切,该语言得到了实质性扩展。在版本7和8中进行了进一步的扩展。我希望这种态度的改变是由C#的崛起驱动的


关于switch(String)的叙述适合于历史,时间轴和上下文cpp / cs。
Espresso'Apr

不实施此功能是一个很大的错误,其他所有都是便宜的借口多年来,由于缺乏进步以及设计师不发​​展语言的顽固态度,Java失去了许多用户。幸运的是,他们在JDK7之后彻底改变了方向和态度
firephil

0

JDK-13中的 JEP 354:开关表达式(预览)和JDK-14中的 JEP 361:开关表达式(标准)将扩展 switch语句,因此可以将其用作表达式

现在你可以:

  • 直接从switch表达式分配变量,
  • 使用新形式的开关标签(case L ->):

    “ case L->”开关标签右侧的代码被限制为表达式,块或(为方便起见)throw语句。

  • 在每种情况下使用多个常量,以逗号分隔,
  • 而且也没有更多的价值突破

    为了从switch表达式中产生一个值,breakwith value语句被删除,转而使用一条yield语句。

因此,从答案(演示12)可能是这样的:

  public static void main(String[] args) {
    switch (args[0]) {
      case "Monday", "Tuesday", "Wednesday" ->  System.out.println("boring");
      case "Thursday" -> System.out.println("getting better");
      case "Friday", "Saturday", "Sunday" -> System.out.println("much better");
    }

-2

不是很漂亮,但这是Java 6和波纹管的另一种方式:

String runFct = 
        queryType.equals("eq") ? "method1":
        queryType.equals("L_L")? "method2":
        queryType.equals("L_R")? "method3":
        queryType.equals("L_LR")? "method4":
            "method5";
Method m = this.getClass().getMethod(runFct);
m.invoke(this);

-3

Groovy轻而易举;我嵌入了groovy jar并创建了一个groovy实用程序类来完成所有这些事情,而我发现这些事情在Java中令人生厌(因为我在企业中一直使用Java 6)。

it.'p'.each{
switch (it.@name.text()){
   case "choclate":
     myholder.myval=(it.text());
     break;
     }}...

9
@SSpoke因为这是一个Java问题,并且Groovy答案是题外话且无用的插件。
马丁

12
即使在保守的大型SW房屋中,Groovy也与Java一起使用。JVM给语言不可知的环境比现在的语言更多,可以混合使用最相关的编程范例作为解决方案。所以也许现在我应该在Clojure中添加一个片段以收集更多的downvotes :) ...
Alex Punnen 2014年

1
另外,语法如何工作?我猜Groovy是另一种编程语言...?抱歉。我对Groovy一无所知。
HyperNeutrino '16

-4

当您使用intellij时,还请注意:

文件->项目结构->项目

文件->项目结构->模块

如果有多个模块,请确保在“模块”选项卡中设置正确的语言级别。


1
不确定您的答案与问题有何关系。他问为什么以下字符串转换语句不可用:String mystring =“ something”; 开关(mystring){情况“东西” sysout(“这里得到”); 。。}
Deepak Agarwal

-8
public class StringSwitchCase { 

    public static void main(String args[]) {

        visitIsland("Santorini"); 
        visitIsland("Crete"); 
        visitIsland("Paros"); 

    } 

    public static void visitIsland(String island) {
         switch(island) {
          case "Corfu": 
               System.out.println("User wants to visit Corfu");
               break; 
          case "Crete": 
               System.out.println("User wants to visit Crete");
               break; 
          case "Santorini": 
               System.out.println("User wants to visit Santorini");
               break; 
          case "Mykonos": 
               System.out.println("User wants to visit Mykonos");
               break; 
         default: 
               System.out.println("Unknown Island");
               break; 
         } 
    } 

} 

8
OP并没有询问如何打开字符串。由于在JDK7之前对语法的限制,他/她在问为什么不能这样做。
HyperNeutrino
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.