Java中字符串switch的实现细节

Java 7中的字符串的switch是如何实现的。验证它其实非常简单,你只需用字符串写一段switch的代码,然后反编译一下,看看编译器是如何翻译它们的就可以了。

 

public class StringInSwitchCase { 
      public static void main(String[] args) { 
            String mode = args[0]; 
            switch (mode) { 
                  case "ACTIVE": 
                        System.out.println("Application is running on Active mode"); 
                        break; 
                  case "PASSIVE":
                        System.out.println("Application is running on Passive mode"); 
                         break; 
                  case "SAFE": 
                          System.out.println("Application is running on Safe mode"); 
          } 
      } 
}

 

生成的字节码如下:

 

  public class StringInSwitchCase{ 
      public StringInSwitchCase() { } 
 
      public static void main(string args[]) { 
             String mode = args[0]; 
            String s; switch ((s = mode).hashCode()) { 
                  default: break; 
                  case -74056953: 
                        if (s.equals("PASSIVE")) { 
                                    System.out.println("Application is running on Passive mode"); 
                         } 
                        break; 
                  case 2537357: 
                        if (s.equals("SAFE")) { 
                              System.out.println("Application is running on Safe mode"); 
                         } 
                        break; 
                  case 1925346054: 
                        if (s.equals("ACTIVE")) { 
                              System.out.println("Application is running on Active mode"); 
                         } 
                        break; 
               } 
          } 
}

看到这个代码,原来字符串的switch是通过equals和hashCode()方法来实现的。

 

switch的case语句可以处理int,short,byte,char类型的值,但是不能处理long类型。因为short,byte,char都会转换成int进行处理,这一点也可以从生成的字节码看出。还好hashCode()方法返回的是int,而不是long。通过这个很容易记住hashCode返回的是int这个事实。仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。

在JDK 5中加入的枚举Enum类型也是可以作为case值的。从字节码可以看出,对于枚举类型也是调用它的ordinal方法转成整型后进行switch匹配的。

 

Type t = Type.C;  
switch (t) {  
    case A:  
        System.out.println("In case A");  
        break;  
    case B:  
        System.out.println("In case B");  
        break;  
    default:  
        System.out.println("In default");  
        break;  
    case C:  
        System.out.println("In case C");  
        break;  
}  

字节码如下:

 

 

   0:   getstatic       #18; //Field com/cdai/jdk/Type.C:Lcom/cdai/jdk/Type;
   3:   astore_1
   4:   invokestatic    #24; //Method $SWITCH_TABLE$com$cdai$jdk$Type:()[I
   7:   aload_1
   8:   invokevirtual   #27; //Method com/cdai/jdk/Type.ordinal:()I
   11:  iaload
   12:  tableswitch{ //1 to 3
                1: 40;
                2: 51;
                3: 73;
                default: 62 }

switch语句有两种编译结果:

 

当case中的值连续时,编译成tableswitch,解释执行时从table数组根据case值计算下标来取值,从数组中取到的便是要跳转的行数。

 

	int i = 3;
	switch (i) {
	case 0:
		System.out.println("0");
		break;
	case 1:
		System.out.println("1");
		break;
	case 3:
		System.out.println("3");
		break;
	case 5:
		System.out.println("5");
		break;
	case 10:
		System.out.println("10");
		break;
	case 13:
		System.out.println("13");
		break;
	case 14:
		System.out.println("14");
		break;
	default:
		System.out.println("default");
		break;
	}

字节码如下:

 

 

Code:
   0:	iconst_3
   1:	istore_1
   2:	iload_1
   3:	tableswitch{ //0 to 14
		0: 76;
		1: 87;
		2: 153;
		3: 98;
		4: 153;
		5: 109;
		6: 153;
		7: 153;
		8: 153;
		9: 153;
		10: 120;
		11: 153;
		12: 153;
		13: 131;
		14: 142;
		default: 153 }
   76:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   79:	ldc	#3; //String 0
   81:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   84:	goto	161
   87:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   90:	ldc	#5; //String 1
   92:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   95:	goto	161
   98:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   101:	ldc	#6; //String 3
   103:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   106:	goto	161
   109:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   112:	ldc	#7; //String 5
   114:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   117:	goto	161
   120:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   123:	ldc	#8; //String 10
   125:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   128:	goto	161
   131:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   134:	ldc	#9; //String 13
   136:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   139:	goto	161
   142:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   145:	ldc	#10; //String 14
   147:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   150:	goto	161
   153:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   156:	ldc	#11; //String default
   158:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   161:	return

其中的

 

3:tableswitch{ //0 to 14
0: 76;
1: 87;
2: 153;
3: 98;
4: 153;
5: 109;
6: 153;
7: 153;
8: 153;
9: 153;
10: 120;
11: 153;
12: 153;
13: 131;
14: 142;
default: 153 }

就是跳转表,对于tableswitch指令,这里high为14,low为0,表中共有high-low+1个分支项,当jvm遇到tableswitch指令时,它会检测switch(key)中的key值是否在low~high之间,如果不是,直接执行default部分,如果在这个范围之内,它使用key-low这个项指定的地点跳转。可见,tableswitch的效率是非常高的。

当case中的值不连续时,编译成lookupswitch,解释执行时需要从头到尾遍历找到case对应的代码行。因为case对应的值不是连续的,如果仍然用表来保存case对应的行号,会浪费大量空间。

	int i = 3;
	switch (i) {
	case 3:
		System.out.println("3");
		break;
	case 20:
		System.out.println("20");
		break;
	case 50:
		System.out.println("50");
		break;
	case 100:
		System.out.println("100");
		break;
	}

字节码如下;

 

Code:
   0:	iconst_3
   1:	istore_1
   2:	iload_1
   3:	lookupswitch{ //4
		3: 44;
		20: 55;
		50: 66;
		100: 77;
		default: 85 }
   44:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   47:	ldc	#3; //String 3
   49:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   52:	goto	85
   55:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   58:	ldc	#5; //String 20
   60:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   63:	goto	85
   66:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   69:	ldc	#6; //String 50
   71:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   74:	goto	85
   77:	getstatic	#2; //Field java/lang/System.out:Ljava/io/PrintStream;
   80:	ldc	#7; //String 100
   82:	invokevirtual	#4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   85:	return

这里使用的是lookupswitch,这种情况下,必须依次检测每一个项目看是否和switch(key) 中的key匹配,如果遇到匹配的直接跳转,如果遇到比key值大的,执行default,因为3,20,50,100这些项目是按照升序排列的,所以遇到比 key值大的case值后就可以确定后面没有符合条件的值了。另外一点,升序排列也允许jvm实现这条指令时进行优化,比如采用二分搜索的方式取代线性扫描等。

 

最后,记住jvm规范中的几句话:
  Compilation of switch statements uses the tableswitch and lookupswitch instructions. The tableswitch instruction is used when the cases of the switch can be efficiently represented as indices into a table of target offsets. The default target of the switch is used if the value of the expression of the switch falls outside the range of valid indices.
  Where the cases of the switch are sparse, the table representation of the tableswitch instruction becomes inefficient in terms of space. The lookupswitch instruction may be used instead.
  The Java virtual machine specifies that the table of the lookupswitch instruction must be sorted by key so that implementations may use searches more efficient than a linear scan. Even so, the lookupswitch instruction must search its keys for a match rather than simply perform a bounds check and index into a table like tableswitch. Thus, a tableswitch instruction is probably more efficient than a lookupswitch where space considerations permit a choice.

由此可以看出,这两种编译结果是编译器在考虑空间占用情况下,对代码效率进行的优化。

另外需要注意一点,就是不管是tableswitch还是lookupswitch,default标签都放在最后进行匹配,但下面各个case和default语句块的顺序是与源代码相同的(但是tableswitch/lookup指令中进行了排序)。所以尽管default标签没有放在最后,但它仍然是最后被匹配的。

switch的转换和具体系统实现有关,可能会使用一种更为高效的方式,叫跳转表。跳转表是一个映射表,存储了可能的值以及要跳转到的地址。
跳转表为什么会更为高效呢?因为,其中的值必须为整数,且按大小顺序排序。按大小排序的整数可以使用高效的二分查找,即先与中间的值比,如果小于中间的值则在开始和中间值之间找,否则在中间值和末尾值之间找,每找一次缩小一倍查找范围。如果值是连续的,则跳转表还会进行特殊优化,优化为一个数组,连找都不用找了,值就是数组的下标索引,直接根据值就可以找到跳转的地址。即使值不是连续的,但数字比较密集,差的不多,编译器也可能会优化为一个数组型的跳转表,没有的值指向default分支。
程序源代码中的case值排列不要求是排序的,编译器会自动排序。之前说switch值的类型可以是byte, short, int, char, 枚举和String。其中byte/short/int本来就是整数,在上节我们也说过,char本质上也是整数,而枚举类型也有对应的整数,String用于switch时也会转换为整数(通过hashCode方法),为什么不可以使用long呢?跳转表值的存储空间一般为32位,容纳不下long。

由于现在的机器性能强劲, 系统的瓶颈更多是IO/DB等,一般不是if else 或者switch,mysql jdbc dirver源码中, 查询结果集resultSet.getObject("aaa")的时候使用的就是if else,  但是个人认为switch代码会使程序易读,更容易维护。

参考:

计算机程序的思维逻辑 (9) - 条件执行的本质

Java Switch语句及性能剖析(转载补充)

Java中字符串switch的实现细节

java中对switch的处理

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值