【问题标题】:Why can't I use switch statement on a String?为什么我不能在字符串上使用 switch 语句?
【发布时间】:2010-09-25 04:34:18
【问题描述】:

此功能是否会被添加到更高的 Java 版本中?

有人能解释一下为什么我不能这样做吗,就像 Java 的 switch 语句的技术工作方式一样?

【问题讨论】:

标签: java string switch-statement


【解决方案1】:

answer 很好地解释了技术细节。我只是想用 Java 12 switch expressions 添加它,您可以使用以下语法:

String translation(String cat_language) {
    return switch (cat_language) {
        case "miau miau" -> "I am to run";
        case "miauuuh" -> "I am to sleep";
        case "mi...au?" ->  "leave me alone";
        default ->  "eat";
    };
} 

【讨论】:

    【解决方案2】:

    在 Java 11+ 中也可以使用变量。唯一的条件是它必须是一个常数。

    例如:

    final String LEFT = "left";
    final String RIGHT = "right";
    final String UP = "up";
    final String DOWN = "down";
    
    String var = ...;
    
    switch (var) {
        case LEFT:
        case RIGHT:
        case DOWN:
        default:
            return 0;
    }
    

    PS。我没有在早期的 jdks 上尝试过这个。因此,如果那里也支持,请更新答案。

    【讨论】:

    • 信息:自版本 7 起,标签必须是“常量表达式”:JLS 14.11
    【解决方案3】:
    JDK-13 中的

    JEP 354: Switch Expressions (Preview) 和 JDK-14 中的 JEP 361: Switch Expressions (Standard) 将扩展 switch 语句,因此它可以用作 表达

    现在你可以:

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

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

    • 每个案例使用多个常量,用逗号分隔,
    • 而且没有更多的价值中断

      为了从 switch 表达式中产生一个值,break with 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");
        }
    

    【讨论】:

      【解决方案4】:

      String 案例的 Switch 语句已在 Java SE 7 中实施,至少 16 年 after they were first requested. 未提供延迟的明确原因,但可能与性能有关。

      在 JDK 7 中的实现

      该功能现已在javac with a "de-sugaring" process; 中实现,在case 声明中使用String 常量的简洁高级语法在编译时按照某种模式扩展为更复杂的代码。生成的代码使用一直存在的 JVM 指令。

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

      JVM 中的开关

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

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

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

      在对String 对象进行switch 去糖时,可能会使用这两个指令。 lookupswitch 适用于第一次打开哈希码找到机箱的原始位置。生成的序数非常适合 tableswitch

      两条指令都要求分配给每个案例的整数常量在编译时进行排序。在运行时,虽然tableswitchO(1) 性能通常看起来好于lookupswitchO(log(n)) 性能,但需要进行一些分析以确定表是否足够密集以证明空间-时间权衡的合理性。 Bill Venners 写的 a great article 对此进行了更详细的介绍,并深入了解了其他 Java 流控制指令。

      JDK 7 之前

      在 JDK 7 之前,enum 可以近似于基于 String 的开关。这使用了编译器在每个enum 类型上生成的the static valueOf 方法。例如:

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

      【讨论】:

      • 对基于字符串的开关使用 If-Else-If 而不是散列可能会更快。我发现只存储少量项目的字典非常昂贵。
      • if-elseif-elseif-elseif-else 可能更快,但我会在 100 次中使用更清晰的代码 99 次。字符串是不可变的,缓存它们的哈希码,所以“计算“哈希很快。必须分析代码以确定有什么好处。
      • 反对添加 switch(String) 的原因是它不能满足 switch() 语句的性能保证。他们不想“误导”开发人员。坦率地说,我认为他们不应该一开始就保证 switch() 的性能。
      • 如果你只是使用Pill 来基于str 采取一些行动,我认为if-else 更可取,因为它允许你处理红色、蓝色范围之外的str 值无需从valueOf 捕获异常或手动检查每个枚举类型的名称是否匹配,这只会增加不必要的开销。根据我的经验,如果稍后需要 String 值的类型安全表示,则使用 valueOf 转换为枚举才有意义。
      • @fernal73 这取决于您级联了多少个 if,以及是否已经计算了 switch 字符串的哈希码。对于两三个人来说,它可能会更快。但在某些时候,switch 语句可能会执行得更好。更重要的是,在很多情况下,switch 语句可能更具可读性。
      【解决方案5】:

      不是很漂亮,但这是 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);
      

      【讨论】:

        【解决方案6】:

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

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

        String 在语言中得到了一些特殊情况处理,但我似乎很清楚设计者试图将特殊大小写和语法糖的数量保持在最低限度。

        在后台切换字符串是相当复杂的,因为字符串不是简单的原始类型。在设计 Java 时,它并不是一个常见的特性,并且与极简主义设计并不完全吻合。特别是当他们决定不对字符串使用特殊大小写 == 时,如果使用 == 不使用 case,那就有点奇怪了。

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

        这一切都随着 Java 5 而改变,语言得到了极大的扩展。在版本 7 和 8 中进行了进一步的扩展。我预计这种态度的变化是由 C# 的兴起推动的

        【讨论】:

        • 关于 switch(String) 的叙述符合历史、时间线、上下文 cpp/cs。
        • 没有实现这个功能是一个很大的错误,其他一切都是Java多年来失去许多用户的廉价借口,因为缺乏进步和设计师顽固不发展语言。好在JDK7之后他们彻底改变了方向和态度
        【解决方案7】:

        如果你没有使用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;
                    }
                }
            }
        }
        

        【讨论】:

        • 两个不同的字符串可以有相同的哈希码,所以如果你打开哈希码,可能会采用错误的大小写分支。
        • @Lii 感谢您指出这一点!不过,这不太可能,但我不相信它会起作用。 “FB”和“Ea”具有相同的哈希码,因此找到冲突并非不可能。第二个代码可能更可靠。
        • 我很惊讶这个编译,因为case 语句必须,我认为,始终是常量值,而String.hashCode() 不是这样(即使在实践中,JVM 之间的计算从未改变)。
        • @StaxMan 嗯,很有趣,我从未停下来观察这一点。但是,是的,case 语句值不必在编译时确定,因此它可以正常工作。
        【解决方案8】:

        还可以显示自 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;
            }
        
        }
        

        【讨论】:

          【解决方案9】:

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

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

          预处理文件被命名为 Foo.jpp 并使用 ant 脚本处理成 Foo.java。

          优点是它被处理成在 1.0 上运行的 Java(尽管通常我们只支持回 1.4)。此外,与使用枚举或其他变通方法进行混淆相比,执行此操作(大量字符串开关)要容易得多 - 代码更易于阅读、维护和理解。 IIRC(目前无法提供统计数据或技术推理)它也比自然的 Java 等价物更快。

          缺点是您没有编辑 Java,因此它需要更多的工作流程(编辑、处理、编译/测试),而且 IDE 会链接回 Java,这有点复杂(开关变成了一系列 if/else逻辑步骤)并且不维护 switch case 顺序。

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

          您可以获取它from SVN 或浏览code online。您需要 EBuild 才能按原样构建它。

          【讨论】:

          • 您不需要 1.7 JVM 来运行带有字符串开关的代码。 1.7 编译器将 String 开关转换为使用先前存在的字节码的东西。
          【解决方案10】:

          以下是基于 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
            }
          }
          

          【讨论】:

            【解决方案11】:

            除了上述好的论点之外,我还要补充一点,今天有很多人将 switch 视为 Java 程序过去(回到 C 时代)的过时剩余部分。

            我并不完全赞同这个观点,我认为switch在某些情况下可以有它的用处,至少是因为它的速度,而且无论如何它比我在一些看到的一些级联数字else if要好代码...

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

            【讨论】:

            • “有时我很遗憾我们没有(匿名)作为一等公民的功能”That's no longer true.
            • @dorukayhan 是的,当然。但是,您是否想在过去十年的所有答案中添加评论,告诉世界如果我们更新到更新版本的 Java,我们可以拥有它们? :-D
            【解决方案12】:

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

            我的观点是,只有当您开始启用非基元时,您需要开始考虑“等于”与“==”。首先比较两个字符串可能是一个相当长的过程,增加了上面提到的性能问题。其次,如果要打开字符串,则需要打开忽略大小写的字符串,打开考虑/忽略语言环境的字符串,打开基于正则表达式的字符串....我会赞成一个可以节省大量时间的决定语言开发人员以程序员的少量时间为代价。

            【讨论】:

            • 从技术上讲,正则表达式已经“切换”,因为它们基本上只是状态机;他们只有两个“案例”,matchednot matched。 (不过,不考虑 [named] groups/etc.)
            • docs.oracle.com/javase/7/docs/technotes/guides/language/… 声明:Java 编译器从使用 String 对象的 switch 语句生成的字节码通常比从链式 if-then-else 语句生成的字节码效率更高。
            【解决方案13】:

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

            当然,您的枚举可以有一个“其他”条目和一个 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;
            }
            

            【讨论】:

            • 此技术还可以让您决定诸如不区分大小写、别名等问题。而不是依赖语言设计者来提供“一刀切”的解决方案。
            • 同意 JeeBee,如果你打开字符串可能需要一个 enum 。该字符串通常表示进入界面(用户或其他)的东西,将来可能会或不会改变,所以最好用枚举替换它
            • 请参阅xefer.com/2006/12/switchonstring,了解有关此方法的精彩文章。
            • @DavidSchmitt 这篇文章有一个重大缺陷。它捕获所有异常,而不是方法实际抛出的异常。
            【解决方案14】:

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

            因此,C 和 C++ 只允许在整数类型上进行切换,因为它对其他类型毫无意义。

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

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

            【讨论】:

            • 使用哈希表可以非常有效地实现基于任何可哈希对象的切换 - 请参阅 .NET。所以你的理由并不完全正确。
            • 是的,这就是我不明白的地方。他们是否担心散列对象从长远来看会变得过于昂贵?
            • @Nalandial:实际上,通过编译器的一些努力,它一点也不昂贵,因为当字符串集已知时,很容易生成完美的哈希(这不是'不过,这不是由 .NET 完成的;可能也不值得付出努力)。
            • @Nalandial & @Konrad Rudolph - 虽然散列字符串(由于它的不可变性质)似乎是解决此问题的方法,但您必须记住,所有非最终对象都可以覆盖其散列函数。这使得在编译时很难确保开关的一致性。
            • 您还可以构造一个 DFA 来匹配字符串(就像正则表达式引擎一样)。可能比散列更有效。
            猜你喜欢
            • 1970-01-01
            • 2023-02-20
            • 2011-02-01
            • 1970-01-01
            • 2012-10-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多