JDK 7 中的语法增强 -- (3) swith 里用 String 类型

java 在 switch 语句这一项上更多的是承袭了 c++,其实整个语法就是 c 风格的。java 的 switch 里只能用 byte、char、short、int 和 enum 类型,连 long 型都不能用(因为 switch 里的要被转换为 int  型,而 long 太长了)。要说不支持 float 和 double 那样的浮点那好理解,因为它们本身是不精确的,1 可能是 0.9999999999。boolean 就两值,放 switch 里无意义,因为 c++ 的 switch 里可用bool 型,但在 java 中遭摈弃。

现在在 jdk7 里 switch 可以用字符串了,学了 c#,字符串的好处就是它是表意的,像 switch(action) case "create" : ...; case "delete" : .....,多写意啊,再也不用先定义一堆常量,然后再 switch(action) case Constants.ACTION_CREATE : ......; case Constants.ACTION_DELETE : ......,当然写过不少程序的尽量会避免写成 case 1... case 2 这样的写法了,会让人不知所以的。常量放在 case 中就要求我们经常要去查它定义的值是什么。

虽然 switch 里可以用字符串了,我想还是应该谨慎些,别搞得以后不易查错,看看:
 1 public String getTypeOfDayWithSwitchStatement(String dayOfWeekArg) {
 2     String typeOfDay;
 3     switch (dayOfWeekArg) {
 4         case "Monday":
 5             typeOfDay = "Start of work week";
 6             break;
 7         case "Tuesday":
 8         case "Wednesday":
 9         case "Thursday":
10             typeOfDay = "Midweek";
11             break;
12         case "Friday":
13             typeOfDay = "End of work week";
14             break;
15         case "Saturday":
16         case "Sunday":
17             typeOfDay = "Weekend";
18             break;
19         default:
20             throw new IllegalArgumentException("Invalid day of the week: " + dayOfWeekArg);
21     }
22     return typeOfDay;
23}

原文中只是说字符串吗,会使用 equals() 方法来比较,然后决定程序的分支走向,并且字符串是区分大小写的,java 编译器对 switch 生成的字节码执行效率上要高于 if-then-else 语句。

为了加深对 switch 中引入字符串支持的理解,我们再进一步,用例子来看看到底发生了什么,java 代码如下:
 1/**
 2 * jdk 7 switch 中使用字符串测试程序
 3 * @author Unmi
 4 */
 5public class Test {
 6
 7    private static int c = 0;
 8
 9    public static void main(String[] args){
10        int i = 21;
11        switch(i){
12            case 3: bar();
13            case 4: bar();
14            default:bar();
15        }
16    }
17
18    public void foo(){
19        String a = "";
20        switch(a){
21            case "3": bar();
22            case "4": bar();
23            default: bar();
24        }
25    }
26
27    public static void bar(){
28    }
29}
由它相应的字节码来帮助理解:
 1Compiled from "Test.java"
 2public class Test extends java.lang.Object {
 3  public Test();
 4    Code:
 5       0: aload_0
 6       1: invokespecial #1                  // Method java/lang/Object."":()V
 7       4: return
 8
 9  public static void main(java.lang.String[]);
10    Code:
11       0: bipush        21
12       2: istore_1
13       3: iload_1
14       4: lookupswitch  { // 2
15                     3: 32
16                     4: 35
17               default: 38
18          }
19      32: invokestatic  #2                  // Method bar:()V
20      35: invokestatic  #2                  // Method bar:()V
21      38: invokestatic  #2                  // Method bar:()V
22      41: return
23
24  public void foo();
25    Code:
26       0: ldc           #3                  // String
27       2: astore_1
28       3: aload_1
29       4: astore_2
30       5: iconst_m1
31       6: istore_3
32       7: aload_2
33       8: invokevirtual #4                  // Method java/lang/String.hashCode:()I
34      11: lookupswitch  { // 2
35                    51: 36
36                    52: 50
37               default: 61
38          }
39      36: aload_2
40      37: ldc           #5                  // String 3
41      39: invokevirtual #6                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
42      42: ifeq          61
43      45: iconst_0
44      46: istore_3
45      47: goto          61
46      50: aload_2
47      51: ldc           #7                  // String 4
48      53: invokevirtual #6                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
49      56: ifeq          61
50      59: iconst_1
51      60: istore_3
52      61: iload_3
53      62: lookupswitch  { // 2
54                     0: 88
55                     1: 91
56               default: 94
57          }
58      88: invokestatic  #2                  // Method bar:()V
59      91: invokestatic  #2                  // Method bar:()V
60      94: invokestatic  #2                  // Method bar:()V
61      97: return
62
63  public static void bar();
64    Code:
65       0: return
66
67  static {};
68    Code:
69       0: iconst_0
70       1: putstatic     #8                  // Field c:I
71       4: return
72}
在 main() 方法中的 switch 语句,判断的是 int 型,与原来的处理办法是一样,是什么值就跳到哪个指令,只会生成一个 lookupswitch 指令。

而在 foo() 方法中的 switch 语句,判断的是字符串,我们看到启用了两条 lookupswitch 指令来支持这一特性,怎么理解它呢?分两步走,从字节码来看似乎有些画蛇添足。

首先它把 case 中的字符串用 hashCode() 算了一些,生成一条 lookupswitch 指令,就是上面的:

  8: invokevirtual #4                  // Method java/lang/String.hashCode:()I
 11: lookupswitch  { // 2
               51: 36
               52: 50
          default: 61
     }

"3" 和 "4" 的 hashCode 分别是 51 和 52,其实这里应该可以像对待整数一样看待字符串,只需要全部转换为相应的 hashCode 即可。不过上面的字令没有那么直接,而是根据传入的 switch 参数,通过 equase() 方法来判断,对原来的 switch 语句重新设定基于 0 开始的整数为参数,也就是对原来的每一个 case 字符串全部转换为整数 0、1、2 ......,最后全部进入到新的 lookupswitch 语句中,从而实现字符串的 switch 功能。

为什么不直接 hashCode 一步完成了,也许这样的效率会比较高吧。

参考:1. http://download.java.net/jdk7/docs/technotes/guides/language/strings-switch.html 永久链接 https://yanbin.blog/jdk-7-enhance-strings-witch/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。