在正文开始之前,先提出一个经典问题:if-else和switch哪一个效率更高?希望你带着问题学习,并在完成本文学习后整理出你的答案。关于if-else控制结构的编译,可以查看我的历史文章。
概述
JVM在编译switch时使用的是tableswitch和lookupswitch指令。这两个指令的编译结果中都会包含一个长度不固定的表,表中成对存放着case值与当前方法中的指令偏移量,我们暂且把这个表叫做:偏移量表。
tableswitch和lookupswitch指令都会从操作数栈中弹出一个key(即switch关键字后面,圆括号中表达式的值)。如果在所有case值中发现了与key值相匹配的,那么case值对应的指令偏移量将会被携带,用于发起指令调用。
case值紧凑——tableswitch
当switch的所有case值比较紧凑,能够有效表示为偏移量表中的索引时,会被编译为tableswitch,反之被编译为lookupswitch。怎样的一组case值才算是紧凑的?请看示例:
Java代码:
int
编译后:
0
如例子中的0、1、2就是紧凑的,紧凑的含义是间距不大,不稀疏。请注意:Java代码中case值的连续性和有序性并不是必须的,接下来把case值改成稀疏非连续的数值再看一看:
Java代码:
int
编译后:
0
当我们把case值改成0、4、1这样的紧凑非连续值时,编译器会自动补充2、3并进行排序,补充的2和3都指向默认的指令偏移量。JVM规定lookupswitch和tableswitch指令的偏移量表必须按照key排序,以便能够实现比线性扫描更加高效的搜索算法,比如二分查找、跳表等.
tableswitch指令格式如下:
tableswitch
指令操作码+0到3字节填充+4字节的默认分支偏移量+4字节的最小case值+4字节的最大case值+若干个有序的4字节分支偏移量。JVM在实现时可以采用跳表的思想,当switch表达式的值位于最小和最大case值范围内时,只需要用表达式值-最小case值,即可得到一个偏移值,通过这个值在jump offsets中直接取到目标分支偏移量。反之,直接获取默认分支偏移量。
0到3字节的填充是为了保证默认分支偏移量在当前方法中的起始地址是4字节的倍数,便于内存对齐,具体填充几个字节由编译器来判断。在现代计算机体系结构中,主存储中的数据会以固定长度数据块的形式被加载到高速缓存中,之后被cpu获取并执行,这个长度可以是4字节/8字节/16字节/32字节/64字节。如果某一条指令的起始地址不是4的倍数,该条指令则需要两次加载才完整,极大地影响了执行效率。
case值稀疏——lookupswitch
当case的值比较稀疏时,编译器并不会自动补充成连续的,因为在空间上会变得低效。
Java代码:
int
编译后:
0
JVM并没有给出稀疏和紧凑的标准定义/判断方法,由编译器实现者自行决定。
lookupswitch指令格式如下:
lookupswitch
指令操作码+0到3字节填充+4字节的默认分支偏移量+若干成对出现的key和分支偏移量值。JVM实现者可以使用二分查找的方式找到匹配的key,并获取对应的指令偏移量。lookupswitch中0到3字节填充的目的和tableswitch中一样。
switch支持的数据类型
站在JVM的视角,无论是lookupswitch还是tableswitch,都只支持int类型(划重点:站在JVM的视角,只支持int类型)。满足要求的数据类型包括:byte、char、short、int,前三者会被内部提升为int类型。
再看看JDK的视角,从JDK1.5开始,switch语句支持了枚举类型,从JDK1.7开始又支持了String类型(划重点:是JDK中的switch支持,并不是JVM中的lookupswitch和tableswitch支持)。之所以能够使用枚举和String类型,是因为枚举类有一个ordinal方法可以返回一个int值,String有一个hashCode方法也可以返回一个int值。编译器在编译代码时,自动加入了一些指令,这些指令用于调用ordinal方法或者hashCode方法,从而获取到tableswitch和lookupswitch能够使用的int类型值。另外,Integer类型也是同样的道理,编译器生成了调用intValue()方法的指令,获取到一个int值。