引言
從 C 到 C++ 到 Java 到一系列各種各樣的語言闹获,大多都支持多路分支語句期犬,比如 Kotlin 的when
和 Rust 的match
等等,在 Java SE 14 版本的語言規(guī)范也添加了對switch
表達式的支持避诽。
本文主要針對 Java SE 8 版本中的switch
語句從字節(jié)碼層面進行研究哭懈,理解switch
語句相關的各種細節(jié),并嘗試著對其編譯產物人工地進行反編譯茎用,探索字符串switch
和枚舉switch
的具體實現(xiàn)方式遣总。
目錄
-
switch
關鍵字基礎 -
switch
所對應的兩種指令 -
switch
字符串的實現(xiàn)原理 -
switch
枚舉的實現(xiàn)原理
1. switch
關鍵字基礎
首先,引用一下語言規(guī)范中的下面幾句話:
The switch Statement
The switch statement transfers control to one of several statements depending on the value of an expression.
The type of the Expression must be char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type, or a compile-time error occurs.
When the switch statement is executed, first the Expression is evaluated. If the Expression evaluates to null, a NullPointerException is thrown and the entire switch statement completes abruptly for that reason. Otherwise, if the result is of a reference type, it is subject to unboxing conversion.
這里提到switch
所接收的表達式參數(shù)必須為char
轨功、byte
旭斥、short
、int
古涧、Character
垂券、Byte
、Short
羡滑、Integer
菇爪、String
、Enum
柒昏,若該表達式參數(shù)為null
凳宙,則拋出 NPE,否則若為引用類型則需要進行拆箱轉換职祷。
實際上switch
在字節(jié)碼層面可以認為它僅僅只支持int
一種類型氏涩,下文我會對此進行解釋。
然后我們可以在這里注意到幾個細節(jié):
這里的參數(shù)類型應是編譯期確定的類型有梆,而不是運行時類型是尖。
這里的參數(shù)在運行時不可為
null
,否則將拋出 NPE泥耀。-
支持
Character
饺汹、Byte
、Short
痰催、Integer
這四種類型的參數(shù)在編譯期將分別對這四種類型的參數(shù)通過
charValue
兜辞、byteValue
、shortValue
陨囊、intValue
這四個方法轉變?yōu)?code>char弦疮、byte
夹攒、short
蜘醋、int
類型,也就是說實質上對于Character
咏尝、Byte
压语、Short
啸罢、Integer
的switch
實質上仍舊是對于char
、byte
胎食、short
扰才、int
的switch
。 -
switch
不支持Long
厕怜、Double
衩匣、Float
、Boolean
類型的參數(shù)通過對上一條的理解粥航,
switch
不支持Long
琅捏、Double
、Float
递雀、Boolean
類型的原因應該是因為switch
不支持long
柄延、double
、float
缀程、boolean
搜吧。 -
switch
不支持long
、double
杨凑、float
類型的參數(shù)因為從
long
滤奈、double
、float
類型向int
類型進行轉換可能會造成損失撩满,所以編譯期不會輕易地將它們隱式轉換為int
類型僵刮,我們只能在自己的源代碼中手動地進行顯式強制類型轉換,才可以將它們轉為int
類型鹦牛。 -
switch
不支持boolean
類型的參數(shù)從
boolean
向int
的轉換是沒有損失的搞糕,但是實際上我們并沒有用switch
對boolean
類型的參數(shù)進行多路分支的必要,畢竟我們可以直接使用if
語句曼追。
到此為止窍仰,我們遺留了幾個主要的問題:
switch
是如何支持int
類型的switch
如何依賴對int
的支持而提供對char
、byte
礼殊、short
的支持switch
指令如何進行多路分支的跳轉switch
如何依賴對int
的支持而提供對String
的支持switch
如何依賴對int
的支持而提供對Enum
的支持
下文將對以上問題進行討論與解答驹吮。
2. switch
所對應的兩種指令
Compilation of switch statements uses the tableswitch and lookupswitch instructions.
The Java Virtual Machine's tableswitch and lookupswitch instructions operate only on int data. Because operations on byte, char, or short values are internally promoted to int, a switch whose expression evaluates to one of those types is compiled as though it evaluated to type int.
switch
語句要使用tableswitch
和lookupswitch
這兩個指令,這兩個指令只針對int
類型進行操作晶伦,而char
碟狞、byte
、short
這三種類型將被隱式轉換為int
婚陪。
如果熟悉 JVM 字節(jié)碼指令集族沃,那么應該很容易理解這兩種switch
僅僅支持int
類型的原因,事實上 JVM 中許多操作都沒有對每種基本類型都專門設計單獨的指令,這是因為 JVM 的所有指令都僅有一個字節(jié)而已脆淹,這樣的好處是不必進行對齊常空,因此效率比較高,但是其弊端就是最多只能提供 256 種指令盖溺,假如真的讓所有操作都同時對boolean
漓糙、char
、float
烘嘱、double
昆禽、byte
、short
蝇庭、int
为狸、long
以及引用這 9 種類型都提供支持,那么 JVM 將最多只能支持二三十種操作遗契,這絕對是不夠用的辐棒,因此許多的操作都僅僅只支持其中的一部分類型,而其余的類型將在運行過程中經(jīng)過類型轉換牍蜂。
2.1 tableswitch
對以下程序進行編譯然后進行反編譯:
class Test {
static int test(int var0) {
switch (var0) {
case 0:
return 0;
case 2:
return 2;
case 3:
return 3;
default:
return -1;
}
}
}
Compiled from "Test.java"
class Test {
Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static int test(int);
Code:
0: iload_0
1: tableswitch { // 0 to 3
0: 32
1: 38
2: 34
3: 36
default: 38
}
32: iconst_0
33: ireturn
34: iconst_2
35: ireturn
36: iconst_3
37: ireturn
38: iconst_m1
39: ireturn
}
上面這個類的文件在我編譯后是這樣的:
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 37 00 10 0A 00 03 00 0D 07 J~:>...7........
00000010: 00 0E 07 00 0F 01 00 06 3C 69 6E 69 74 3E 01 00 ........<init>..
00000020: 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 .()V...Code...Li
00000030: 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 04 neNumberTable...
00000040: 74 65 73 74 01 00 04 28 49 29 49 01 00 0D 53 74 test...(I)I...St
00000050: 61 63 6B 4D 61 70 54 61 62 6C 65 01 00 0A 53 6F ackMapTable...So
00000060: 75 72 63 65 46 69 6C 65 01 00 09 54 65 73 74 2E urceFile...Test.
00000070: 6A 61 76 61 0C 00 04 00 05 01 00 04 54 65 73 74 java........Test
00000080: 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A ...java/lang/Obj
00000090: 65 63 74 00 20 00 02 00 03 00 00 00 00 00 02 00 ect.............
000000a0: 00 00 04 00 05 00 01 00 06 00 00 00 1D 00 01 00 ................
000000b0: 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 07 .....*7..1......
000000c0: 00 00 00 06 00 01 00 00 00 01 00 08 00 08 00 09 ................
000000d0: 00 01 00 06 00 00 00 5C 00 01 00 01 00 00 00 28 .......\.......(
000000e0: 1A AA 00 00 00 00 00 25 00 00 00 00 00 00 00 03 .*.....%........
000000f0: 00 00 00 1F 00 00 00 25 00 00 00 21 00 00 00 23 .......%...!...#
00000100: 03 AC 05 AC 06 AC 02 AC 00 00 00 02 00 07 00 00 .,.,.,.,........
00000110: 00 16 00 05 00 00 00 03 00 20 00 05 00 22 00 07 ............."..
00000120: 00 24 00 09 00 26 00 0B 00 0A 00 00 00 06 00 04 .$...&..........
00000130: 20 01 01 01 00 01 00 0B 00 00 00 02 00 0C ..............
從 0x0x000000e1 至 0x0x000000ff 的這 31 個字節(jié)便是tableswitch
漾根。
按照tableswitch的解釋,tableswitch
即 0xAA鲫竞,其后分別跟隨 default辐怕、low、high从绘,在本例中即分別為37寄疏、0、3僵井,再其后跟隨其余的 high - low + 1 即 4 個 offset陕截,在本例中分別為 31、37批什、33农曲、35。
這里需要注意一個細節(jié)驻债,那就是在 0xAA 與 default 之間存在 0 至 3 個填充字節(jié)乳规,目的在于保證 default、low合呐、high 以及后面的每一個 offset 在 class 文件中相對于這個方法的第一個指令的地址偏移都是 4 的倍數(shù)暮的,在本例中填充字節(jié)有 2 個,該方法的第一個指令的地址是 0x000000e0淌实。
tableswitch
的int
參數(shù)從操作數(shù)棧中彈出冻辩,并直接查表然后跳轉猖腕。此處應注意到上面的源碼中并不存在case 1
,但是在字節(jié)碼中卻在case 1
的位置存在一個和 default 相等的值微猖。
這是因為tableswitch
需要直接查表獲取即將跳轉的地址偏移谈息,這個偏移從 0xAA 的位置開始計算缘屹,所以這個映射表必須支持隨機讀取凛剥,也就是說所有的case
都要在 low 至 high 的范圍內順序排列,若參數(shù)小于 low 或大于 high 則跳轉到 default 的位置轻姿,而在 table 內部被填充的表項也對應此 default 值犁珠。
tableswitch
看起來應十分高效,因為虛擬機可以直接查表從而得到對應的 offset互亮,但是這里也有一個缺陷犁享,也就是上一段剛剛提到的,需要保證每個case
的連續(xù)豹休,即使源碼中并不存在這個case
炊昆,也要在編譯后填充。因此tableswitch
僅適用于switch
的case
相對來說比較密集的情況下威根,而在其比較稀疏的情況下則不應使用tableswitch
而應使用lookupswitch
凤巨。
2.2 lookupswitch
對以下程序進行編譯然后進行反編譯:
class Test {
static int test(int var0) {
switch (var0) {
case 1000:
return 0;
case 100:
return 1;
case 10:
return 2;
default:
return -1;
}
}
}
Compiled from "Test.java"
class Test {
Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static int test(int);
Code:
0: iload_0
1: lookupswitch { // 3
10: 40
100: 38
1000: 36
default: 42
}
36: iconst_0
37: ireturn
38: iconst_1
39: ireturn
40: iconst_2
41: ireturn
42: iconst_m1
43: ireturn
}
上面這個類的文件在我編譯后是這樣的:
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 37 00 10 0A 00 03 00 0D 07 J~:>...7........
00000010: 00 0E 07 00 0F 01 00 06 3C 69 6E 69 74 3E 01 00 ........<init>..
00000020: 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 .()V...Code...Li
00000030: 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 04 neNumberTable...
00000040: 74 65 73 74 01 00 04 28 49 29 49 01 00 0D 53 74 test...(I)I...St
00000050: 61 63 6B 4D 61 70 54 61 62 6C 65 01 00 0A 53 6F ackMapTable...So
00000060: 75 72 63 65 46 69 6C 65 01 00 09 54 65 73 74 2E urceFile...Test.
00000070: 6A 61 76 61 0C 00 04 00 05 01 00 04 54 65 73 74 java........Test
00000080: 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A ...java/lang/Obj
00000090: 65 63 74 00 20 00 02 00 03 00 00 00 00 00 02 00 ect.............
000000a0: 00 00 04 00 05 00 01 00 06 00 00 00 1D 00 01 00 ................
000000b0: 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 07 .....*7..1......
000000c0: 00 00 00 06 00 01 00 00 00 01 00 08 00 08 00 09 ................
000000d0: 00 01 00 06 00 00 00 60 00 01 00 01 00 00 00 2C .......`.......,
000000e0: 1A AB 00 00 00 00 00 29 00 00 00 03 00 00 00 0A .+.....)........
000000f0: 00 00 00 27 00 00 00 64 00 00 00 25 00 00 03 E8 ...'...d...%...h
00000100: 00 00 00 23 03 AC 04 AC 05 AC 02 AC 00 00 00 02 ...#.,.,.,.,....
00000110: 00 07 00 00 00 16 00 05 00 00 00 03 00 24 00 05 .............$..
00000120: 00 26 00 07 00 28 00 09 00 2A 00 0B 00 0A 00 00 .&...(...*......
00000130: 00 06 00 04 24 01 01 01 00 01 00 0B 00 00 00 02 ....$...........
00000140: 00 0C ..
從 0x0x000000e1 至 0x0x00001003 的這 35 個字節(jié)便是lookupswitch
。
按照lookupswitch的解釋洛搀,lookupswitch
即0xAB敢茁,其后分別跟隨 default、npairs留美,在本例中即分別為41彰檬、3,再其后跟隨其余的 npairs 即 3 組映射谎砾,在本例中分別為10逢倍、39、100景图、37瓶堕、1000、35症歇。
lookupswitch
也需要填充 0 至 3 個字節(jié)郎笆,本例中是 2 個,該方法第一個指令的地址為 0x000000e0忘晤。
lookupswitch
的int
參數(shù)也從操作數(shù)棧中彈出宛蚓,并在這些case
中查找匹配項。在我上面的源碼中case
順序是 1000设塔、100凄吏、10,但是在字節(jié)碼中卻是 10、100痕钢、1000图柏,這是因為lookupswitch
要求所有 offset 應以遞增的順序排列,從而使 JVM 可以支持比線性更高效的查找方式任连,比如二分查找蚤吹,但是規(guī)范中在此并沒有要求虛擬機所實現(xiàn)的查找方式。
由于lookupswitch
需要進行查找随抠,而不能像tableswitch
那樣直接查表裁着,因此或許效率會有所降低,但是在case
相對比較稀疏的情況下拱她,比起tableswitch
二驰,lookupswitch
將節(jié)省大量的空間。
對于我們在代碼中的switch
語句秉沼,選擇lookupswitch
或是tableswitch
將依賴于編譯器的具體實現(xiàn)桶雀。
3. switch
字符串的實現(xiàn)原理
以下代碼作為示例:
class Test {
static int test(String var0) {
switch (var0) {
case "foo":
return 0;
case "bar":
return 1;
case "10":
return 2;
case "0O":
return 3;
default:
return -1;
}
}
}
編譯后的字節(jié)碼:
Compiled from "Test.java"
class Test {
Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static int test(java.lang.String);
Code:
0: aload_0
1: astore_1
2: iconst_m1
3: istore_2
4: aload_1
5: invokevirtual #2 // Method java/lang/String.hashCode:()I
8: lookupswitch { // 3
1567: 72
97299: 58
101574: 44
default: 97
}
44: aload_1
45: ldc #3 // String foo
47: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
50: ifeq 97
53: iconst_0
54: istore_2
55: goto 97
58: aload_1
59: ldc #5 // String bar
61: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
64: ifeq 97
67: iconst_1
68: istore_2
69: goto 97
72: aload_1
73: ldc #6 // String 0O
75: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
78: ifeq 86
81: iconst_3
82: istore_2
83: goto 97
86: aload_1
87: ldc #7 // String 10
89: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
92: ifeq 97
95: iconst_2
96: istore_2
97: iload_2
98: tableswitch { // 0 to 3
0: 128
1: 130
2: 132
3: 134
default: 136
}
128: iconst_0
129: ireturn
130: iconst_1
131: ireturn
132: iconst_2
133: ireturn
134: iconst_3
135: ireturn
136: iconst_m1
137: ireturn
}
人工地反編譯:
class Test {
static int test(String var0) {
String var1 = var0;
int var2 = -1;
switch (var1.hashCode()) {
case 101574:
if (var1.equals("foo")) {
var2 = 0;
}
break;
case 97299:
if (var1.equals("bar")) {
var2 = 1;
}
break;
case 1567:
if (var1.equals("0O")) {
var2 = 3;
} else if (var1.equals("10")) {
var2 = 2;
}
break;
}
switch (var2) {
case 0:
return 0;
case 1:
return 1;
case 2:
return 2;
case 3:
return 3;
default:
return -1;
}
}
}
在我的環(huán)境下,這和上面的字符串switch
編譯結果幾乎完全一致唬复,javap -c
無任何區(qū)別矗积。
對字符串的switch
實際上被拆分成了 5 個步驟:
調用參數(shù)字符串的
hashCode
方法獲取 hash 值對其 hash 值進行第一次
switch
調用
equals
方法并修改狀態(tài)碼對狀態(tài)碼進行第二次
switch
執(zhí)行對應分支中的語句
這里注意幾個細節(jié):
若參數(shù)字符串為
null
,則調用hashCode
方法將拋出 NPE盅抚,符合虛擬機規(guī)范的要求漠魏。狀態(tài)碼的默認值為 -1,若參數(shù)字符串沒有匹配任何一個
case
字面量妄均,則會保持不變柱锹,否則將被修改為對應case
在源代碼中的序號,從 0 開始計算丰包。-
case
字符串字面量的 hash 值需要在編譯期經(jīng)過計算并寫入 class 字節(jié)碼文件中禁熏,而參數(shù)字符串的hashCode
方法需要在運行時調用才能夠得到結果,這就要求同一個字符串的 hash 算法必須在編譯期和運行時是對應的邑彪,否則經(jīng)過第一次switch
后狀態(tài)碼將被賦予錯誤的值瞧毙,于是在第二次switch
將走入錯誤的分支路徑中,執(zhí)行錯誤的邏輯寄症。由于虛擬機會對被加載的類進行版本驗證宙彪,因此 hash 算法的一致在類加載的流程中可以被虛擬機所保證。
-
僅對參數(shù)字符串的 hash 進行
switch
不足以確定其是否與case
字面量匹配有巧,因此至少會調用一次equals
方法释漆。比如上例中
"0O"
和"10"
這兩個字符串的 hash 值相同,所以在完成對 hash 值的switch
后篮迎,要以這兩個case
的倒序逐個調用equals
方法并判斷結果男图。此處可以參考 Java 8 中
java.lang.String
的源碼:
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
自定義類的 hash 算法在編譯期是無法確定的示姿,因此switch
不能以和字符串字面量相同的方式實現(xiàn)自定義類的多路分支。
4. switch
枚舉的實現(xiàn)原理
以下代碼作為示例:
enum Foobar {
FOO,
BAR;
}
class Test {
static int test(Foobar var0) {
switch (var0) {
case FOO:
return 1;
case BAR:
return 2;
default:
return 0;
}
}
}
編譯后的字節(jié)碼:
Compiled from "Test.java"
class Test {
Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static int test(Foobar);
Code:
0: getstatic #2 // Field Test$1.$SwitchMap$Foobar:[I
3: aload_0
4: invokevirtual #3 // Method Foobar.ordinal:()I
7: iaload
8: lookupswitch { // 2
1: 36
2: 38
default: 40
}
36: iconst_1
37: ireturn
38: iconst_2
39: ireturn
40: iconst_0
41: ireturn
}
Compiled from "Test.java"
class Test$1 {
static final int[] $SwitchMap$Foobar;
static {};
Code:
0: invokestatic #1 // Method Foobar.values:()[LFoobar;
3: arraylength
4: newarray int
6: putstatic #2 // Field $SwitchMap$Foobar:[I
9: getstatic #2 // Field $SwitchMap$Foobar:[I
12: getstatic #3 // Field Foobar.FOO:LFoobar;
15: invokevirtual #4 // Method Foobar.ordinal:()I
18: iconst_1
19: iastore
20: goto 24
23: astore_0
24: getstatic #2 // Field $SwitchMap$Foobar:[I
27: getstatic #6 // Field Foobar.BAR:LFoobar;
30: invokevirtual #4 // Method Foobar.ordinal:()I
33: iconst_2
34: iastore
35: goto 39
38: astore_0
39: return
Exception table:
from to target type
9 20 23 Class java/lang/NoSuchFieldError
24 35 38 Class java/lang/NoSuchFieldError
}
人工地反編譯:
class Test {
static int test(Foobar var0) {
switch (Test$1.$SwitchMap$Foobar[var0.ordinal()]) {
case 1:
return 1;
case 2:
return 2;
default:
return 0;
}
}
}
class Test$1 {
static final int[] $SwitchMap$Foobar;
static {
$SwitchMap$Foobar = new int[Foobar.values().length];
try {
$SwitchMap$Foobar[Foobar.FOO.ordinal()] = 1;
} catch (NoSuchFieldError e) {
;
}
try {
$SwitchMap$Foobar[Foobar.BAR.ordinal()] = 2;
} catch (NoSuchFieldError e) {
;
}
}
}
在我的環(huán)境下逊笆,這和上面的枚舉switch
編譯結果幾乎完全一致栈戳,唯一的不同之處是在自動生成的那個匿名內部類中并沒有包私有無參構造函數(shù),但手動編寫的類會被自動添加一個包私有無參構造函數(shù)难裆。
這個 Test$1 類是自動生成的包私有靜態(tài)匿名內部類子檀,匿名類的名字都是其外部類的名字加'$'
加數(shù)字,'$'
在每層嵌套類的名字之間作為間隔符差牛,數(shù)字則是為匿名類自動生成的名字命锄,其值在編譯期被確定堰乔,因此可能在迭代過程中隨著項目版本的變化而變化偏化,本例中這個數(shù)字為 1。
這里可以發(fā)現(xiàn)镐侯,在 Test 類中對 Foobar 枚舉的switch
被轉變成了對 Test$1.$SwitchMap$Foobar[var0.ordinal()] 這一整數(shù)表達式的switch
侦讨,這里 Test$1 的靜態(tài)成員 $SwitchMap$Foobar 也在編譯期自動生成,這是一個int
數(shù)組苟翻,被選取的參數(shù)在這個數(shù)組中的索引是這個 Foobar 對象的ordinal
方法的返回值韵卤。
ordinal
方法的返回值是這個枚舉常量在其聲明時的序號。這里參考一下Enum
類的源碼:
/**
* The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*
* Most programmers will have no use for this field. It is designed
* for use by sophisticated enum-based data structures, such as
* {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*/
private final int ordinal;
/**
* Returns the ordinal of this enumeration constant (its position
* in its enum declaration, where the initial constant is assigned
* an ordinal of zero).
*
* Most programmers will have no use for this method. It is
* designed for use by sophisticated enum-based data structures, such
* as {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*
* @return the ordinal of this enumeration constant
*/
public final int ordinal() {
return ordinal;
}
這里注意若枚舉參數(shù)為null
則調用ordinal
方法會拋出 NPE崇猫,符合虛擬機規(guī)范的要求沈条。
$SwitchMap$Foobar 數(shù)組的長度與在 Test 類中所依賴的那個版本的枚舉 Foobar 類的枚舉常量個數(shù)相同,這個數(shù)組在這個自動生成的包私有靜態(tài)匿名內部類被加載時被初始化诅炉,分別取出在 Test 類中所依賴的那個版本的枚舉 Foobar 類的所有枚舉常量蜡歹,并在它們的ordinal
的位置設置ordinal + 1
的值。
因此涕烧,當在 Test 類中對 Test$1.$SwitchMap$Foobar[var0.ordinal()] 這個int
類型的表達式進行switch
時月而,若這個int
參數(shù)為正數(shù),則可以跳轉到所對應的路徑议纯,并執(zhí)行對應的語句父款,否則將跳轉到default
分支,在這種情況下參數(shù)通常為默認值0
瞻凤。
這里要注意一下憨攒,在 Test$1 類中調用了 Foobar 類的靜態(tài)方法values
,它是在 Foobar 被編譯時自動生成的阀参。
這里稍微分析一下 Foobar 這個枚舉類:
源代碼:
enum Foobar {
FOO,
BAR;
}
字節(jié)碼:
Compiled from "Foobar.java"
final class Foobar extends java.lang.Enum<Foobar> {
public static final Foobar FOO;
public static final Foobar BAR;
public static Foobar[] values();
Code:
0: getstatic #1 // Field $VALUES:[LFoobar;
3: invokevirtual #2 // Method "[LFoobar;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[LFoobar;"
9: areturn
public static Foobar valueOf(java.lang.String);
Code:
0: ldc #4 // class Foobar
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class Foobar
9: areturn
static {};
Code:
0: new #4 // class Foobar
3: dup
4: ldc #7 // String FOO
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field FOO:LFoobar;
13: new #4 // class Foobar
16: dup
17: ldc #10 // String BAR
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field BAR:LFoobar;
26: iconst_2
27: anewarray #4 // class Foobar
30: dup
31: iconst_0
32: getstatic #9 // Field FOO:LFoobar;
35: aastore
36: dup
37: iconst_1
38: getstatic #11 // Field BAR:LFoobar;
41: aastore
42: putstatic #1 // Field $VALUES:[LFoobar;
45: return
}
反編譯:
final class Foobar extends java.lang.Enum<Foobar> {
public static final Foobar FOO;
public static final Foobar BAR;
private static final Foobar[] $VALUES;
public static Foobar[] values() {
return (Foobar[]) $VALUES.clone();
}
public static Foobar valueOf(java.lang.String) {
return (Foobar) java.lang.Enum.valueOf(Foobar.class, name);
}
private Foobar(String name, int ordinal) {
super(name, ordinal);
}
static {
FOO = new Foobar("FOO", 0);
BAR = new Foobar("BAR", 1);
$VALUES = new Foobar[] { FOO, BAR };
}
}
假如 Test 還存在內部類肝集,并且我們再定義了另外的兩個枚舉類 Foo 和 Bar,那么無論是在 Test 中對 Foo 和 Bar 參數(shù)執(zhí)行switch
结笨,還是在 Test 的內部類中對 Foo 和 Bar 參數(shù)執(zhí)行switch
包晰,都會依賴這同一個 Test$1 類湿镀。
假如在 Test 及其內部類的源代碼中,同時對枚舉類 Foo 和 枚舉類 Bar 執(zhí)行了switch
伐憾,那么在 Test$1 類中將同時存在 $SwitchMap$Foo 和 $SwitchMap$Bar 這兩個int
數(shù)組勉痴。
如果兩個不同的類 A 和類 B 都對枚舉類 Foobar 執(zhí)行了switch
,或是分別對枚舉類 Foo 和枚舉類 Bar 執(zhí)行了switch
树肃,無論如何只要這兩個對枚舉執(zhí)行switch
的類 A 和類 B 之間沒有嵌套關系蒸矛,那么就會分別生成 A$1 和 B$1 這兩個類,哪怕 A$1 和 B$1 這兩個類只有類名不同胸嘴,也仍然會重復生成雏掠。
重復地生成幾乎不變的冗余代碼,會無謂地增多類的個數(shù)劣像、增大包體積乡话、浪費類加載的時間,但是實際上編譯器這樣處理是有必要的耳奕。
這里我們需要著重理解绑青,F(xiàn)oobar、Test$1屋群、Test 這三個類之間的依賴關系:
Foobar 是在 Test 中被switch
的枚舉實例所屬的類闸婴,在 Test 類被編譯時,編譯器將自動生成一個依賴于這個版本的 Test$1 類芍躏。雖然 Test$1 類依賴 Foobar 類的定義邪乍,但Test 類僅僅依賴 Foobar 類的聲明,這就意味著即使在 Test$1 類編譯完成后对竣,F(xiàn)oobar 類被修改并且重新編譯庇楞,但是并沒有重新編譯 Test 類,那么 Test$1 將不會因 Foobar 的改變而得到更新的機會柏肪,因為它只有在 Test 類被編譯時才能夠被編譯姐刁。
上面這段話也許有些饒舌,那么我還是以一段代碼作為實例:
首先編譯 Foobar:
enum Foobar {
FOO,
BAR;
}
javac Foobar.java
然后編譯 Test:
class Test {
static int test(Foobar var0) {
switch (var0) {
case FOO:
return 1;
case BAR:
return 2;
default:
return 0;
}
}
}
javac Test.java
然后修改 Foobar:
enum Foobar {
FOOBAR,
NIL;
}
javac Foobar.java
然后執(zhí)行以下程序:
class Main {
public static void main(String[] args) {
System.out.println(Test.test(Foobar.FOOBAR));
}
}
javac Main.java
java Main
結果為 0烦味。
以上操作建議使用命令行聂使,但是使用 IDE 重現(xiàn)一遍也許印象會更為深刻:
- 在 foobar 項目中將 Foobar 類作為 JAR 打包發(fā)布第一個版本
- 在 test 項目中的
pom.xml
或者build.gradle
中添加對第一個版本的 foobar 項目的依賴并將 Test 類和 Test$1 類作為 JAR 打包發(fā)布 - 在 foobar 項目中修改 Foobar 類的代碼并作為 JAR 打包發(fā)布第二個版本
- 在 main 項目中的
pom.xml
或者build.gradle
中添加對 test 項目的依賴以及對第二個版本的 foobar 項目的依賴隨后運行 Main 類的 main 函數(shù)
無論使用命令行還是 IDE,在手動重現(xiàn)了這一過程后谬俄,我想應該已經(jīng)能夠大致理解我的意思柏靶,我在此還是展開解釋:
當 Test$1 類被加載的時候,將按照 Test 類編譯時所依賴的版本的 Foobar 類的枚舉常量的名字為 $SwitchMap$Foobar 數(shù)組初始化溃论,假如沒能找到這個枚舉常量屎蜓,則會拋出NoSuchFieldError
并在 Test$1 中被捕獲且忽視,于是在 $SwitchMap$Foobar 數(shù)組的對應位置將要維持初始值 0钥勋,因此在 Test 類中將因這個 0 值的存在而走向default
分支炬转,從而確保在項目所依賴的 SDK 之間的版本匹配錯誤的情況下辆苔,程序還可以保證自己的健壯性,避免因走入未知的分支而威脅業(yè)務安全扼劈。