閱讀完本文我相信大家會有不少收貨的,如果遇到不懂的地方,請耐心查閱相關知識。
JDK7之后switch為什么可以支持String類型的條件判斷
記得讀大學教我們Java課程的老師曾說,switch判斷條件的數據類型只支持int和char。但是現在看來,這句話就不是那么嚴謹了,因為JDK7之后,還支持String類型的判斷條件。接下來分析一步步分析其中的原理。
示例代碼
public class TestSwitch {
public static final java.lang.String CASE_ONE = "1";
public static final java.lang.String CASE_TWO = "2";
public static final java.lang.String CASE_THERE = "3";
public static final java.lang.String CASE_FOUR = "4";
public void testSwitch(String key) {
switch (key) {
case CASE_ONE:
break;
case CASE_TWO:
break;
case CASE_THERE:
break;
case CASE_FOUR:
break;
default:
break;
}
}
}
接著通過以下命令,將該Java文件轉成Class文件
javac TestSwtich.java
然后通過以下命令,將編譯后的Class文件進行反編譯
javap TesTSwitch.class
(注意:以上javac和javap命令是JDK工具提供的,如有不了解的可以通過javac -help和javap -help進行了解)
得到如下匯編代碼。
public class TestSwitch {
public static final java.lang.String CASE_ONE;
public static final java.lang.String CASE_TWO;
public static final java.lang.String CASE_THERE;
public static final java.lang.String CASE_FOUR;
public TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
//這里開始是分析的重點
public void testSwitch(java.lang.String);
Code:
//將方法參數key加載進操作數棧
0: aload_1
//接著將該方法參數key存儲到局部變量表
1: astore_2
//將int常量-1壓入操作數棧中
2: iconst_m1
//接著將剛壓入棧中的常量-1存儲到局部變量表中
3: istore_3
//將局部變量表中的存儲的方法參數key加載到操作數棧頂
4: aload_2
//這一步是關鍵,接著虛擬機會調用String的hashCode方法,
//即對示例源碼中key值進行hashCode,這樣做的目的就是轉成對int類型的判斷
5: invokevirtual #2 // Method java/lang/String.hashCode:()I
//taleswitch是Java虛擬機對應Java源碼中switch關鍵字的指令
8: tableswitch { // 49 to 52
//49對應著常量字符串“1”的hashCode值,也是字符‘1’的ASCII值,
//大家可以看下String類hasCode的源碼就會知道為什么相等了。
//如果字符串key的hashCode值等于常量字符串“1”的hashCode值,
//則跳轉到行號為40的地方繼續執行指令
49: 40
50: 54
51: 68
52: 82
default: 93
}
//將局部變量表中的存儲的方法參數key加載到操作數棧頂
40: aload_2
// 將常量池中的常量字符串“1”壓入棧中
41: ldc #3
//接著調用String的equals方法將常量字符串“1”和key進行比較,接著講返回值壓入棧頂
//雖然equals方法的返回值是布爾類型,但是Java虛擬機會將布爾類型窄化成int型。
43: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
//從棧頂中彈出int型數據,如果為0則跳轉到行號為93的地方進行執行,0代表false
46: ifeq 93
//將int型常量0壓入棧中
//為什么會把0壓入棧中?因為虛擬機會將第一個case情況默認賦值為0,后面的case情況依次+1
49: iconst_0
//將int型常量0存儲到局部變量表中
50: istore_3
//接著跳轉到行號為93的地方繼續執行
51: goto 93
54: aload_2
55: ldc #5 // String 2
57: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
60: ifeq 93
63: iconst_1
64: istore_3
65: goto 93
68: aload_2
69: ldc #6 // String 3
71: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
74: ifeq 93
77: iconst_2
78: istore_3
79: goto 93
82: aload_2
83: ldc #7 // String 4
85: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
88: ifeq 93
91: iconst_3
92: istore_3
//以上每種case情況,最終會跳轉到該行指令執行
//上面分析中,如果case情況為字符串“1”,則會保存一個int常量0,
//這里0也就代表了case為“1”的情況。
//這句指令會把先前存儲在局部變量表中的int值加載到棧頂
93: iload_3
94: tableswitch { // 0 to 3
//如果等于0,則跳轉到124行指令處執行
0: 124
1: 127
2: 130
3: 133
default: 136
}
124: goto 136
127: goto 136
130: goto 136
133: goto 136
136: return
}
以上代碼中,我分析了"case CASE_ONE:"情況,雖然我們Java代碼中只用了一個switch關鍵字,但是編譯器生成的字節碼卻用了兩個tableswitch指令。第一條tableswitch指令是根據字符串key哈希之后的int值進行調整判斷,跳轉到相應的行號之后,接著調用equals方法進行字符串內容比較,如果內容相等,會將每種case情況用一個int值記錄,從0開始依次加1。第二條tableswitch指令會根據每種case情況所對應的int值進行判斷,最終轉化為switch的判斷條件為int類型的情況。
由此可見,用String類型作為判斷條件,編譯器編譯后的指令也會相應的增加。因此建議,能夠用int值作為判斷條件的就用int值吧。
如果對Java虛擬機指令不了解的,請耐心翻閱相關書籍或查閱相關資料。我相信你也會有不少收貨的。
如何寫出高效的switch代碼
看到這個標題,不要驚訝。我們邊寫示例邊分析原理。
示例代碼1
public class TestSwitch {
public static final int CASE_ONE = 1;
public static final int CASE_TWO = 2;
public static final int CASE_THERE = 3;
public void testSwitch(int key) {
switch (key) {
case CASE_ONE:
break;
case CASE_TWO:
break;
case CASE_THERE:
break;
default:
break;
}
}
}
反編譯后得到:
public class TestSwitch {
public static final int CASE_ONE;
public static final int CASE_TWO;
public static final int CASE_THERE;
public TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void testSwitch(int);
Code:
0: iload_1
1: tableswitch { // 1 to 3
1: 28
2: 31
3: 34
default: 37
}
28: goto 37
31: goto 37
34: goto 37
37: return
}
示例代碼2
“示例代碼2”在“示例代碼1”的基礎上,僅僅修改了最后一個case情況的判斷常量數的值。從“3”變成“5"。
public class TestSwitch {
public static final int CASE_ONE = 1;
public static final int CASE_TWO = 2;
public static final int CASE_FIVE = 5;
public void testSwitch(int key) {
switch (key) {
case CASE_ONE:
break;
case CASE_TWO:
break;
case CASE_FIVE:
break;
default:
break;
}
}
}
反編譯后得到
public class TestSwitch {
public static final int CASE_ONE;
public static final int CASE_TWO;
public static final int CASE_FIVE;
public TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void testSwitch(int);
Code:
0: iload_1
1: tableswitch { // 1 to 5
//“示例代碼2”相對“示例代碼1”來說僅僅改變了最后一個case的判斷常量數的值。
//但是會導致所有case判斷常量數的值不連續了?!笆纠a1”是“1,2,3”,這個三個數是連續的。
//但是本示例中變成了“1,2,5”,這個三個數就不連續了。tableswitch這個指令比較“聰明”的。
//如果判斷數值是不連續的,且又不是那么離散,那么它會自動把中間缺的判斷常量數給補上。
//例如下面代碼,判斷條件3和4是編譯器幫我們補上的。
//補上后有什么好處呢?補上后,“1,2,3,4,5”這些判斷條件值就是一個連續值的整型數組,利于判斷的直接跳轉。
//比如我們傳入的判斷條件數值是3,即switch(key)中key值為3,
//那么虛擬機會首先判斷3是否在1-5之間,
//如果在則取目標值1(即下面“1:36”的這行代碼)為參照,
//接著直接跳轉到數組中(3-1)項(注意:數組是從0開始),即“3:45”代碼出。
1: 36
2: 39
3: 45
4: 45
5: 42
default: 45
}
36: goto 45
39: goto 45
42: goto 45
45: return
}
示例代碼3
public class TestSwitch {
public static final int CASE_ONE = 1;
public static final int CASE_TWO = 2;
public static final int CASE_TEN = 10;
public void testSwitch(int key) {
switch (key) {
case CASE_ONE:
break;
case CASE_TWO:
break;
case CASE_TEN:
break;
default:
break;
}
}
}
反編譯后得到
public class TestSwitch {
public static final int CASE_ONE;
public static final int CASE_TWO;
public static final int CASE_TEN;
public TestSwitch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void testSwitch(int);
Code:
0: iload_1
1: lookupswitch { // 3
//這里我們將最后一個判斷條件的數值改為10后,
//這里出現的不是tableswitch指令,而是lookupswitch指令了。
//原因是,case中所有的判斷條件的數值比較離散了,如果系統繼續幫我們補其剩余判斷數的話(即從3到9),
//那么會浪費不少內存空間。因此這里改用lookupswitch指令,那么該指令是如何執行呢?
//其實很簡單就是一步一步的比較下去,知道碰到條件滿足的。
1: 36
2: 39
10: 42
default: 45
}
36: goto 45
39: goto 45
42: goto 45
45: return
}
這三種情況我只改變了最后一個case的判斷常量數的值,但是得到的反編譯代碼卻有所不同。
對于編譯器來說,switch的case判斷條件值,有三種情況。
1.判斷值都是連續數字。
2.判斷數字不連續但也不很離散。
3.判斷數字比較離散。
以上第一種和第二種情況是使用tableswitch指令,第三種情況使用lookupswitch指令。
這里大家可能會對第二種情況有些許疑問,就是如何判斷不連續但也不很離散,這里虛擬機有一套判斷規則,會根據switch語句的case個數和case的所有判斷常量數值的離散情況而定。
簡而言之,tableswitch指令是以空間換時間來提供效率,而lookupswitch會“犧牲”效率來換取空間。但是我們不需要考慮這些,因為聰明的編譯器會幫我們搞定。
使用switch關鍵字的建議。
switch是我們經常打交道的關鍵字,但是在寫判斷條件的時候我們不應該很隨意設置。
比如
public class TestSwitch {
public static final int DO_SWIM = 1;
public static final int DO_EAT = 2;
public static final int DO_DRINK = 3;
……
public void testSwitch(int key) {
switch (key) {
case DO_SWIM:
break;
case DO_EAT:
break;
case DO_DRINK:
break;
……
default:
break;
}
}
}
DO_SWIM、DO_EAT、DO_DRINK……,我們分別代表三種不用行為,如果分別設置為1、2、3……這種連續數據。那么這種代碼就是相當完美的。但是有時候有些程序員比較“另類”,可能會設成1,3,5,7……這種純"奇"數據?;蛘?0,100,1000,10000……這種霸氣數據。后兩種情況的數據要么是會造成空間浪費,要么是會影響執行效率。因此,我們在寫switch指令時,如果情況允許,最好將case的判斷數據設置為連續的,同時對減少apk體積也有那么點幫助。
寫在最后
小小的代碼背后都可能會蘊含高深的原理,保持一顆求知的心,我們才能不斷進步,共勉之!當然,如果文章有何錯誤或寫的不明白之處,歡迎大家指出,非常感謝閱讀!