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

这个功能是否会在以后的Java版本中使用?

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

769728 次浏览

String案例的Switch语句已在JavaSE 7中实现,至少16年在他们第一次被要求之后。没有提供延迟的明确原因,但它可能与性能有关。

JDK 7中的实现

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

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

JVM中的开关

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

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

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

String对象上去糖switch时,可能会使用两种指令。lookupswitch适用于第一次打开哈希码以找到案例的原始位置。结果序数自然适合tableswitch

这两条指令都要求在编译时对分配给每种情况的整数常量进行排序。在运行时,虽然tableswitchO(1)性能通常看起来比lookupswitchO(log(n))性能更好,但它需要一些分析来确定表是否足够密集以证明时空权衡是合理的。比尔·维纳斯写了一篇伟大的文章,更详细地介绍了这一点,以及对其他Java流控制指令的暗中研究。

在JDK 7之前

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

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

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

出于这个原因,C&C++只允许在整数类型上切换,因为它对其他类型毫无意义。

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

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

如果您在代码中有一个可以打开String的地方,那么最好将String重构为可以打开的可能值的枚举。当然,您可以将String的潜在值限制为枚举中的值,这可能是所需的,也可能不是所需的。

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

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

我的观点是,只要你开始切换非原语,你就需要开始考虑“等于”与 "==". 首先,比较两个字符串可能是一个相当漫长的过程,增加了上面提到的性能问题。其次,如果有切换字符串,就会有切换字符串的需求忽略case,切换考虑/忽略区域设置的字符串,切换基于正则表达式的字符串……我赞成这样一个决定,它为语言开发人员节省了大量时间,但为程序员节省了少量时间。

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

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

但实际上,值得考虑一下需要开关的情况,看看它是否不能被更OO的东西取代。例如Java1.5+中的枚举,也许是HashTable或其他一些集合(有时我很遗憾我们没有(匿名)函数作为一等公民,就像Lua一样-没有开关-或JavaScript)甚至是多态性。

下面是一个基于JeeBee帖子的完整示例,使用java枚举而不是使用自定义方法。

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

多年来,我们一直在为此使用(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很少安装最新版本),它会很有用。

您可以获取从svn或浏览代码在线。您需要EBuild才能按原样构建它。

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

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

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

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

String在语言中有一些特殊的大小写处理,但在我看来很明显,设计师试图将特殊大小写和语法糖的数量保持在最低限度。

打开字符串本质上是相当复杂的,因为字符串不是简单的原始类型。在Java设计的时候,这不是一个常见的特性,也不太适合极简主义设计。特别是当他们决定不为字符串设置特殊情况==时,在==不起作用的情况下使用case会有点奇怪。

在1.0到1.4之间,语言本身基本保持不变。对Java的大多数增强都在库端。

这一切都随着Java5而改变,该语言得到了实质性的扩展。随后在版本7和8中进行了进一步的扩展。我预计这种态度的变化是由C#的兴起驱动的

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

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

JDK-13中的JEP 354:切换表达式(预览)和JDK-14中的JEP 361:切换表达式(标准)将扩展开关语句,因此它可以用作<强>表情

现在您可以:

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

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

  • 每个case使用多个常量,用逗号分隔,
  • 也没有更多的值打破

    为了从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");}

在Java11+中,变量也是可能的。唯一的条件是它必须是常量。

例如:

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上尝试过这个。所以如果那里也支持,请更新答案。

技术细节在这个回答中很好地解释了。我只是想补充一下,在Java12开关表达式中,您可以使用以下语法来完成:

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