日常編碼中,我們常常用到switch語句,在我的另外一篇文章中【JAVA】優化if else的幾種方式,也談到了可以利用switch來優化if-else結構,那麼switch底層究竟是如何實現的呢?
我們先寫幾個示例
第一個示例:case條件中的int值連續
public int switchInt(int i) {
int result;
switch (i) {
case 0:
result = 10;
break;
case 1:
result = 20;
break;
case 2:
result = 30;
break;
default:
result = 40;
}
return result;
}
老規矩,反編譯後得到:
(首先javac Main.java,之後javap -c -p Main.class)
public class com.yang.testSwitch.Main {
public com.yang.testSwitch.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int switchInt(int);
Code:
0: iload_1 //將局部變量表下標爲1的元素,也就是傳入的i的值壓入棧頂
1: tableswitch { // 0 to 2 //從棧頂中彈出元素,檢查是否在[0,2]之內
0: 28 //如果爲0,則程序計數器跳轉到第28行
1: 34 //如果爲1,則程序計數器跳轉到第34行
2: 40 //如果爲2,則程序計數器跳轉到第40行
default: 46 //如果不在[0,2]內,則程序計數器跳轉到第46行
}
28: bipush 10 //將常量10壓入棧頂
30: istore_2 //將棧頂元素10存入局部變量表的第3個位置上
31: goto 49 //跳轉到底49行
34: bipush 20
36: istore_2
37: goto 49
40: bipush 30
42: istore_2
43: goto 49
46: bipush 40
48: istore_2
49: iload_2 //將局部變量表中的第3個元素壓入棧中
50: ireturn //彈出棧頂元素,方法返回
}
其中的tableswitch是後面會着重分析的內容,這裏先放着。
這裏有一個疑問:
爲什麼剛開始傳進來的i的值,是存到局部變量表中的第二個位置,第一個位置到底放了啥?
因爲該方法是一個實例方法,局部變量表的第一個位置總是存放着this引用。在構造方法中,已經使用aload_0將this應用存入到了局部變量表中的第一個位置上。
關於這些字節碼指令,可以參考這篇文章jvm理論-字節碼指令
第二個示例:case中的int值不連續
public int switchInt(int i) {
int result;
switch (i) {
case 0:
result = 10;
break;
case 3:
result = 20;
break;
case 7:
result = 30;
break;
default:
result = 40;
}
return result;
}
現在改成0,3,7了,繼續觀察反編譯後得到的內容:
public class com.yang.testSwitch.Main {
public com.yang.testSwitch.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int switchInt(int);
Code:
0: iload_1
1: lookupswitch { // 3
0: 36
3: 42
7: 48
default: 54
}
36: bipush 10
38: istore_2
39: goto 57
42: bipush 20
44: istore_2
45: goto 57
48: bipush 30
50: istore_2
51: goto 57
54: bipush 40
56: istore_2
57: iload_2
58: ireturn
}
僅僅改動了case中的值,由連續改成了不連續,得到的字節碼指令居然發生了變化,由tableswitch變成了lookupswitch。
看到這裏,我們可以總結一下
tableswitch和lookupswitch兩者的區別
tableswitch
tableswitch用來處理條件連續的情況。首先進行一次範圍檢查,檢查不通過便直接返回default。檢查通過後,會得到對應case語句的偏移量。
tableswitch使用數組結構存儲偏移量,因此利用下標可以快速定位到偏移量,時間複雜度爲O(N)。
注意:這裏的“連續”可以理解爲相對連續或半連續,012連續,013可以理解爲半連續。013也會使用tableswitch,只不過裏面缺少的2和default分支會有同樣的偏移量。
lookupswitch
lookupswitch則用來處理條件不連續的情況,當條件大面積不連續時,tableswitch將會產生大量的額外空間。使用lookupswitch,會將case值進行排序,之後可以利用二分法快速查到對應的分支偏移量。
lookupswitch則是維護了一個經過key排序的(key,value)結構,查找複雜度一般爲O(logN)。
第三個示例:使用String類型
在jdk1.7的時候,可以使用String類型作爲case條件了。
public int switchString(String str) {
int result;
switch (str) {
case "a":
result = 10;
break;
case "c":
result = 20;
break;
case "f":
result = 30;
break;
default:
result = 40;
}
return result;
}
這次我們不直接看字節碼指令,僅僅觀察class文件:
public class Main {
public Main() {
}
public int switchString(String var1) {
byte var4 = -1;
switch(var1.hashCode()) {
case 97:
if (var1.equals("a")) {
var4 = 0;
}
break;
case 99:
if (var1.equals("c")) {
var4 = 1;
}
break;
case 102:
if (var1.equals("f")) {
var4 = 2;
}
}
byte var2;
switch(var4) {
case 0:
var2 = 10;
break;
case 1:
var2 = 20;
break;
case 2:
var2 = 30;
break;
default:
var2 = 40;
}
return var2;
}
}
可以看得出,主要流程爲:
- 首先使用hashcode方法獲取到字符串的哈希值
- 因爲case條件中的字符串的哈希值大概率不連續,所以字節碼指令一般是使用lookupswitch來保存對應的偏移量
- 之後執行偏移量的字節碼指令,一上來會調用equals來判斷var1是否與case條件中的String相匹配(因爲可能存在兩個不同字符串的哈希值相同的情況)
- 匹配成功後,會再利用tableswitch來查詢var4代表的偏移量
- 最後返回var2
總的來說,使用String類型來作爲case中的條件,本質上還是先轉化爲hashcode,接着查到對應的hashcode,最後再利用equals檢測內容是否相同。
第四個示例:使用枚舉類型
public enum Animal {
CAT, DOG
}
public int switchEnum(Animal animal) {
int result;
switch (animal) {
case CAT:
result = 1;
break;
case DOG:
result = 2;
break;
default:
result = 3;
}
return result;
}
編譯該文件會得到3個文件,分別是
- Main.class 這個文件是肯定有的
- Main$Animal.class 這個是枚舉類,也是正常的
- Main$1.class 這是個啥?
進到Main$1.class發現:
import com.yang.testSwitch.Main.Animal;
// $FF: synthetic class
class Main$1 {
static {
try {
$SwitchMap$com$yang$testSwitch$Main$Animal[Animal.CAT.ordinal()] = 1;
} catch (NoSuchFieldError var2) {
}
try {
$SwitchMap$com$yang$testSwitch$Main$Animal[Animal.DOG.ordinal()] = 2;
} catch (NoSuchFieldError var1) {
}
}
}
使用javap -p Main$1.class後:
class com.yang.testSwitch.Main$1 {
static final int[] $SwitchMap$com$yang$testSwitch$Main$Animal;
static {};
}
原來這個class裏,聲明瞭一個靜態的數組,數組利用枚舉的ordinal()值作爲下標,數組中的元素依次遞增。
那這個數組到底是幹啥用的呢?
我們接着使用javap -c -p Main.class反編譯得到:
public class com.yang.testSwitch.Main {
public com.yang.testSwitch.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int switchEnum(com.yang.testSwitch.Main$Animal);
Code:
0: getstatic #2 // Field com/yang/testSwitch/Main$1.$SwitchMap$com$yang$testSwitch$Main$Animal:[I
3: aload_1
4: invokevirtual #3 // Method com/yang/testSwitch/Main$Animal.ordinal:()I
7: iaload
8: lookupswitch { // 2
1: 36
2: 41
default: 46
}
36: iconst_1
37: istore_2
38: goto 48
41: iconst_2
42: istore_2
43: goto 48
46: iconst_3
47: istore_2
48: iload_2
49: ireturn
}
現在可以發現,首先是獲取到這個靜態數組,再調用枚舉的ordinal()方法獲取枚舉的值,再將這個值當作靜態數組的下標,獲取這個靜態數組中的某一個元素,然後從lookupswitch中尋找偏移量。
第五個示例:使用包裝類型
public int switchByte(Byte i) {
int result;
switch (i) {
case 1:
result = 10;
break;
case 2:
result = 20;
break;
case 3:
result = 30;
break;
default:
result = 40;
}
return result;
}
反編譯得到:
public class com.yang.testSwitch.Main {
public com.yang.testSwitch.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int switchByte(java.lang.Byte);
Code:
0: aload_1
1: invokevirtual #2 // Method java/lang/Byte.byteValue:()B
4: tableswitch { // 1 to 3
1: 32
2: 38
3: 44
default: 50
}
32: bipush 10
34: istore_2
35: goto 53
38: bipush 20
40: istore_2
41: goto 53
44: bipush 30
46: istore_2
47: goto 53
50: bipush 40
52: istore_2
53: iload_2
54: ireturn
}
可以很清晰的看到,調用了Byte.byteValue()方法完成了對Byte的拆箱工作,之後比較byte值即可。
總結
我們用一個表格,來直觀的說明各種類型的底層操作:
類型 |
操作 |
基本類型 |
直接比較(條件半連續,採用tableswitch,否則lookupswitch) |
String |
首先獲取hashcode,之後採用equals判斷 |
Enum |
自動生成SwitchMap數組,下標是枚舉的ordinal(),值是從1開始遞增的整數 |
包裝類型 |
先進行拆箱,之後比較基本類型 |
這裏有幾點需要注意的是:
(1)根據java虛擬機規範,java虛擬機的tableswitch和lookupswitch指令都只能支持int類型的條件值,所以switch不支持long、float、double與boolean,以及他們對應的包裝類。
(2)break語句會在字節碼層面生成一個goto語句,用來直接跳轉出switch語句塊。因此,如果有哪一次忘記寫break,那麼程序執行就會進入下一個case中,可能會造成某些匪夷所思的問題。