Java語法糖的編譯結果分析(二)
語法糖(Syntactic Sugar)的出現是為了降低我們編寫某些程式碼時陷入的重複或繁瑣,這使得我們使用語法糖後可以寫出簡明而優雅的程式碼。在Java中不加工的語法糖程式碼執行時可不會被虛擬機器接受,因此編譯器為了讓這些含有語法糖的程式碼正常工作其實需要對這些程式碼進行加工,經過編譯器在生成class位元組碼的階段完成解語法糖(desugar)的過程,那麼這些語法糖最終究竟被編譯成了什麼呢,在這裡列舉了如下的一些Java典型的語法糖,結合例項和它們的編譯結果分析一下。本文為本系列第二篇。
列舉類
列舉在編譯後會變成一個特殊的final類,因此列舉型別是名副其實的不可變類,我們通過下面最簡單的例子來仔細分析一下:
原始碼:
enum COLOR {
RED,
BLUE,
GREEN
}
使用這個列舉的時候我們可以發現有valueOf(String)
和values()
這樣的方法可以用,因此不難猜測編譯器會新增一些未在原始碼中出現的其他增強二進位制位元組碼,可以看一下具體的位元組碼:
final class COLOR extends java.lang.Enum<COLOR> minor version: 0 major version: 52 flags: ACC_FINAL, ACC_SUPER, ACC_ENUM ... { public static final COLOR RED; descriptor: LCOLOR; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final COLOR BLUE; descriptor: LCOLOR; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final COLOR GREEN; descriptor: LCOLOR; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static COLOR[] values(); descriptor: ()[LCOLOR; flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #1 // Field $VALUES:[LCOLOR; 3: invokevirtual #2 // Method "[LCOLOR;".clone:()Ljava/lang/Object; 6: checkcast #3 // class "[LCOLOR;" 9: areturn public static COLOR valueOf(java.lang.String); descriptor: (Ljava/lang/String;)LCOLOR; flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: ldc #4 // class COLOR 2: aload_0 3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; 6: checkcast #4 // class COLOR 9: areturn static {}; descriptor: ()V flags: ACC_STATIC Code: stack=4, locals=0, args_size=0 0: new #4 // class COLOR 3: dup 4: ldc #7 // String RED 6: iconst_0 7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 10: putstatic #9 // Field RED:LCOLOR; 13: new #4 // class COLOR 16: dup 17: ldc #10 // String BLUE 19: iconst_1 20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 23: putstatic #11 // Field BLUE:LCOLOR; 26: new #4 // class COLOR 29: dup 30: ldc #12 // String GREEN 32: iconst_2 33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 36: putstatic #13 // Field GREEN:LCOLOR; 39: iconst_3 40: anewarray #4 // class COLOR 43: dup 44: iconst_0 45: getstatic #9 // Field RED:LCOLOR; 48: aastore 49: dup 50: iconst_1 51: getstatic #11 // Field BLUE:LCOLOR; 54: aastore 55: dup 56: iconst_2 57: getstatic #13 // Field GREEN:LCOLOR; 60: aastore 61: putstatic #1 // Field $VALUES:[LCOLOR; 64: return } Signature: #32 // Ljava/lang/Enum<LCOLOR;>;
這段位元組碼可以證實出上面的猜測,確實會有額外的二進位制位元組碼被添加了,列舉類會被編譯成為Ljava/lang/Enum
的子類COLOR
,而列舉型別中的列舉項會被編譯成為COLOR
類的常量欄位,而且COLOR
內部還會維護一個數組來儲存這些常量欄位,並進而新增valueOf(String)
和values()
來訪問這個陣列。因此,對應地我們可以翻譯這段二進位制位元組碼為這樣的程式碼:
final class COLOR extends Enum<COLOR> { private static final COLOR RED; private static final COLOR BLUE; private static final COLOR GREEN; private static final COLOR[] $VALUES; static { RED = new COLOR("RED", 0); BLUE = new COLOR("BLUE", 1); GREEN = new COLOR("GREEN", 2); COLOR[] $COLOR_ARRAY = new COLOR[3]; $COLOR_ARRAY[0] = RED; $COLOR_ARRAY[1] = BLUE; $COLOR_ARRAY[2] = GREEN; $VALUES = $COLOR_ARRAY; } private COLOR(String color, int ordinal) { super(color, ordinal); } public static COLOR[] values() { return $VALUES.clone(); } public static COLOR valueOf(String color) { return Enum.valueOf(COLOR.class, color); } }
注意,這段程式碼並不能通過編譯,因為原始碼這一層是不允許直接繼承Ljava/lang/Enum
的,這個繼承過程只允許在編譯器內部解語法糖的過程中被編譯器新增,新增之後的類才會有ACC_ENUM
的訪問識別符號。
我們可以看到的是在Ljava/lang/Enum
內部實際上有name
和ordinal
常量來標識一個列舉項,name
會由列舉項名來設定,而ordinal
是列舉項序號,由列舉項排列順序決定。
我們再來看一下帶有欄位的列舉項編譯後的效果。
原始碼:
enum COLOR {
RED(0),
BLUE(1),
GREEN(2);
int code;
COLOR(int code) {
this.code = code;
}
}
編譯後的位元組碼:
final class COLOR extends java.lang.Enum<COLOR>
minor version: 0
major version: 52
flags: ACC_FINAL, ACC_SUPER, ACC_ENUM
...
{
public static final COLOR RED;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final COLOR BLUE;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final COLOR GREEN;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
int code;
descriptor: I
flags:
public static COLOR[] values();
descriptor: ()[LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #1 // Field $VALUES:[LCOLOR;
3: invokevirtual #2 // Method "[LCOLOR;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[LCOLOR;"
9: areturn
LineNumberTable:
line 1: 0
public static COLOR valueOf(java.lang.String);
descriptor: (Ljava/lang/String;)LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #4 // class COLOR
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class COLOR
9: areturn
LineNumberTable:
line 1: 0
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: new #4 // class COLOR
3: dup
4: ldc #8 // String RED
6: iconst_0
7: iconst_0
8: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
11: putstatic #10 // Field RED:LCOLOR;
14: new #4 // class COLOR
17: dup
18: ldc #11 // String BLUE
20: iconst_1
21: iconst_1
22: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
25: putstatic #12 // Field BLUE:LCOLOR;
28: new #4 // class COLOR
31: dup
32: ldc #13 // String GREEN
34: iconst_2
35: iconst_2
36: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
39: putstatic #14 // Field GREEN:LCOLOR;
42: iconst_3
43: anewarray #4 // class COLOR
46: dup
47: iconst_0
48: getstatic #10 // Field RED:LCOLOR;
51: aastore
52: dup
53: iconst_1
54: getstatic #12 // Field BLUE:LCOLOR;
57: aastore
58: dup
59: iconst_2
60: getstatic #14 // Field GREEN:LCOLOR;
63: aastore
64: putstatic #1 // Field $VALUES:[LCOLOR;
67: return
LineNumberTable:
line 2: 0
line 3: 14
line 4: 28
line 1: 42
}
Signature: #36 // Ljava/lang/Enum<LCOLOR;>;
用java原始碼翻譯下上面的結果:
final class COLOR extends Enum<COLOR> {
private static final COLOR RED;
private static final COLOR BLUE;
private static final COLOR GREEN;
int code;
private static final COLOR[] $VALUES;
static {
RED = new COLOR("RED", 0, 0);
BLUE = new COLOR("BLUE", 1, 1);
GREEN = new COLOR("GREEN", 2, 2);
COLOR[] $COLOR_ARRAY = new COLOR[3];
$COLOR_ARRAY[0] = RED;
$COLOR_ARRAY[1] = BLUE;
$COLOR_ARRAY[2] = GREEN;
$VALUES = $COLOR_ARRAY;
}
private COLOR(String color, int ordinal, int code) {
super(color, ordinal);
this.code = code;
}
public static COLOR[] values() {
return $VALUES.clone();
}
public static COLOR valueOf(String color) {
return Enum.valueOf(COLOR.class, color);
}
}
其實有了之前的基礎很容易看出來,新增加的code
欄位最終只是變成了編譯器生成的COLOR
類的一個欄位,唯一的變化就是編譯出的初始化方法也會增加為這個欄位而新增的引數。
斷言
java 1.4引入的斷言,使用關鍵字assert
來判斷一個條件是否為true,通過如下的原始碼來分析一下:
class Main {
public static void main(String[] args) {
String judge = "yes";
assert "no".equals(judge);
}
}
斷言在執行時預設是關閉的,我們可以通過執行時開啟斷言來啟用:java -ea Main
:
at Main.main(Main.java:4)
那麼我們來看一下編譯後的位元組碼:
{
static final boolean $assertionsDisabled;
descriptor: Z
flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
Main();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: ldc #2 // String yes
2: astore_1
3: getstatic #3 // Field $assertionsDisabled:Z
6: ifne 26
9: ldc #4 // String no
11: aload_1
12: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
15: ifne 26
18: new #6 // class java/lang/AssertionError
21: dup
22: invokespecial #7 // Method java/lang/AssertionError."<init>":()V
25: athrow
26: return
StackMapTable: number_of_entries = 1
frame_type = 252 /* append */
offset_delta = 26
locals = [ class java/lang/String ]
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #8 // class Main
2: invokevirtual #9 // Method java/lang/Class.desiredAssertionStatus:()Z
5: ifne 12
8: iconst_1
9: goto 13
12: iconst_0
13: putstatic #3 // Field $assertionsDisabled:Z
16: return
StackMapTable: number_of_entries = 2
frame_type = 12 /* same */
frame_type = 64 /* same_locals_1_stack_item */
stack = [ int ]
}
可以發現編譯器為Main
類添加了欄位$assertionsDisabled
,此欄位即是啟用斷言的關鍵。在執行時加入啟用斷言的-ea
會使得類初始化時Class.desiredAssertionStatus
為真,進而欄位$assertionsDisabled
為真,這個邏輯在上述的位元組碼中可以看出。在斷言的地方,如果條件為真則會正常返回,如果條件為false
則會丟擲java/lang/AssertionError
錯誤導致程式終止。
用java原始碼翻譯下上面的結果:
class Main {
private static final boolean $assertionsDisabled;
static {
if (Main.class.desiredAssertionStatus()) {
$assertionsDisabled = true;
} else {
$assertionsDisabled = false;
}
}
public static void main(String[] args) {
if($assertionsDisabled) {
if (!"no".equals("yes")) {
throw new AssertionError();
}
}
}
}
switch處理列舉和字串
我們先來看看在java 1.7以前就可以使用switch的型別在位元組碼層是如何工作的,這裡以int
型別為例:
class Main {
public static void main(String[] args) {
int a = 1;
switch (a) {
case 0:
System.out.println("0");
break;
case 2:
System.out.println("1");
break;
case 8:
System.out.println("3");
break;
default:
break;
}
}
}
編譯後的位元組碼:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: lookupswitch { // 3
0: 36
2: 47
8: 58
default: 69
}
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #3 // String 0
41: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: goto 69
47: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
50: ldc #5 // String 1
52: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
55: goto 69
58: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #6 // String 3
63: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: goto 69
69: return
}
這裡是用的位元組碼命令lookupswitch
適用於判斷switch的跳轉語句的,即如果switch 0
跳轉到26行、switch 2
跳轉到47行、switch 8
跳轉到58行、其他跳轉到69行。受限於lookupswitch
判斷的條件的型別,在java 1.7以前是無法對非32位數字型別的型別做判斷的,而java 1.7以後通過語法糖的解析實現了字串的switch分支判斷,可以想到的是,在不改變lookupswitch
的能力的情況下,編譯器會將字串轉換為32位數字。我們寫這樣的例子來分析下:
class Main {
public static void main(String[] args) {
String a = args[0];
switch (a) {
case "a":
System.out.println("a");
break;
case "b":
System.out.println("b");
break;
default:
break;
}
}
}
編譯後的結果:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: aload_0
1: iconst_0
2: aaload
3: astore_1
4: aload_1
5: astore_2
6: iconst_m1
7: istore_3
8: aload_2
9: invokevirtual #2 // Method java/lang/String.hashCode:()I
12: lookupswitch { // 2
97: 40
98: 54
default: 65
}
40: aload_2
41: ldc #3 // String a
43: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
46: ifeq 65
49: iconst_0
50: istore_3
51: goto 65
54: aload_2
55: ldc #5 // String b
57: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
60: ifeq 65
63: iconst_1
64: istore_3
65: iload_3
66: lookupswitch { // 2
0: 92
1: 103
default: 114
}
92: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
95: ldc #3 // String a
97: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
100: goto 114
103: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
106: ldc #5 // String b
108: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
111: goto 114
114: return
LocalVariableTable:
Start Length Slot Name Signature
0 115 0 args [Ljava/lang/String;
4 111 1 a Ljava/lang/String;
}
我們可以發現編譯器對要做分支判斷的字串計算了它的hashcode,而這個hashcode是符合lookupswitch
要求的32位數字,因此將這個hashcode做lookupswitch
分支判斷,和switch條件中的"a"
、"b"
的hashcode做比較,如果進入了其中某個分支如"a"
分支,則在分支中判斷"a"
和字串是否相等,如果相等則確定此分支是正確的(只有hashcode相等並不能確定是值相等,hashcode的衝突原理不再展開),接下來再將分支條件直接設定為0、1、2這樣的簡單條件執行下一輪lookupswitch
。我們同樣可以用如下java原始碼翻譯下上面的結果:
class Main {
public static void main(String[] args) {
String param = args[0];
int hashcode = param.hashCode();
final int condition_a = 97; //"a".hashCode()
final int condition_b = 98; //"b".hashCode();
int hashcodeSwitchResult = -1;
switch (hashcode) {
case condition_a:
if("a".equals(param)){
hashcodeSwitchResult = 0;
}
break;
case condition_b:
if("b".equals(param)){
hashcodeSwitchResult = 1;
}
break;
default:
break;
}
switch (hashcodeSwitchResult) {
case 0:
System.out.println("a");
break;
case 1:
System.out.println("b");
break;
default:
break;
}
}
}