轉載請註明原創出處,謝謝!
Java 語言中主要的語法糖有:泛型、自動裝箱、自動拆箱、遍歷循環、變長參數、條件編譯、內部類、和 try 語句中定義和關閉資源等。本文主要通過跟蹤 javac 源碼、反編譯 Class 文件等方式去了解它們的本質實現。
語法糖(Syntactic sugar),也譯爲糖衣語法,是由英國計算機科學家彼得·約翰·蘭達(Peter J. Landin)發明的一個術語,指計算機語言中添加的某種語法,這種語法對語言的功能並沒有影響,但是更方便程序員使用。通常來說使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。摘自:百度百科
1、泛型和類型擦除
泛型是 JDK 1.5 的一項新增特性,它的本質是參數化類型(Prametersized Type)的應用,也就是說說操作的數據類型被指定爲一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口和泛型方法。
Java 語言中泛型是一種僞泛型。怎麼理解呢?它只在程序源碼中存在,在編譯之後的字節碼文件中,就已經替換成原生類型了,並在相應的地方插入了強制轉型代碼。對於運行期的 Java 語言來說,ArrayList 和 ArrayList 就是同一個類。
至於 Java 爲什麼要這樣子實現,可以參考一下知乎裏面 R大 的回答
Java不能實現真正泛型的原因?
下面我們來看一個例子:
/* 編譯前 Java 代碼 */
public class GenericSyntacticSugar {
public void test() {
List<String> names = new ArrayList<>();
names.add("a");
String name = names.get(0);
}
}
將上面這段代碼編譯成 Class 文件:
/* 編譯後 Class 文件 */
public class GenericSyntacticSugar {
public GenericSyntacticSugar() {
}
public void test() {
ArrayList var1 = new ArrayList(); // 這裏已經沒有 <String> 類型了
var1.add("a");
String var2 = (String)var1.get(0); // 使用的時候添加強制轉型
}
}
可以看到,在編譯後的 Class 文件中,泛型不見了!!!並且在使用的地方加了強制轉型。這也就是我們常說的類型擦除。
2、自動裝箱與自動拆箱
自動裝箱是 Java 編譯器在基本類型和相應的對象包裝類之間進行的自動轉換。例如,將 int 轉換爲 Integer ,將 double 轉換爲 Double ,等等。如果轉換以另一種方式進行,例如,將 Integer 轉換爲 int,這稱爲自動拆箱。
自動裝箱發生的條件:當一個原始類型變量
(1)作爲參數傳遞給期望對應包裝器類的對象的方法。
(2)分配給相應包裝器類的變量。
相應的,自動拆箱觸發於:當一個包裝器類變量
(1)作爲參數傳遞給期望對應基元類型值的方法。
(2)分配給相應基元類型的變量。
下面我們來看一個例子:
/* 編譯前 Java 代碼 */
public class Autoboxing {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a + b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));
}
}
至於這段代碼的輸出是什麼?去掉語法糖之後代碼是什麼樣子?讀者可以帶着這兩個問題繼續讀下去。
首先通過 javac Autoboxing.java
得到 Autoboxing.class 文件(由於 Class 文件中並不能非常明顯的看出自動裝箱與拆箱,因此根據字節碼來查看具體發生了什麼),
隨後通過 javap -c Autoboxing.class
得到下列字節碼。
/* 反編譯 Java 字節碼 */
public class com.yhh.example.syntactic.sugar.Autoboxing {
public com.yhh.example.syntactic.sugar.Autoboxing();
Code:
0: aload_0
// ----調用超類構造方法,即父類的構造函數
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
// ----將int型1推送至棧頂
0: iconst_1
// ----調用靜態方法 Integer.valueOf()
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
// ----將棧頂引用型數值存入第二個本地變量
4: astore_1
// ----0 - 4 步相當於代碼中的 Integer a = 1; => Integer a = Integer.valueOf(1); 發生了自動裝箱。
5: iconst_2
6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
// ----Integer b = 2; => Integer b = Integer.valueOf(2); 發生了自動裝箱。
10: iconst_3
11: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: astore_3
// ----Integer c = 3; => Integer c = Integer.valueOf(3); 發生了自動裝箱。
15: iconst_3
16: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: astore 4
// ----Integer d = 3; => Integer d = Integer.valueOf(3); 發生了自動裝箱。
21: sipush 321
24: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
27: astore 5
// ----Integer e = 321; => Integer e = Integer.valueOf(321); 發生了自動裝箱。
29: sipush 321
32: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
35: astore 6
// ----Integer f = 321; => Integer f = Integer.valueOf(321); 發生了自動裝箱。
37: ldc2_w #3 // long 3l
40: invokestatic #5 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
43: astore 7
// ----Long g = 3L; => Long g = Long.valueOf(3); 發生了自動裝箱。
45: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
// ----將第四個引用類型本地變量推送至棧頂
48: aload_3
// ----將第五個引用類型本地變量推送至棧頂
49: aload 4
// ----比較棧頂兩引用型數值,當結果不相等時跳轉至 58
51: if_acmpne 58
// ----將int型1推送至棧頂
54: iconst_1
// ----無條件跳轉至 59
55: goto 59
// ----將int型0推送至棧頂
58: iconst_0
// ----調用實例方法 System.out.println
59: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
// ----45 - 59 步對應於:System.out.println(c == d);
62: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
65: aload 5
67: aload 6
69: if_acmpne 76
72: iconst_1
73: goto 77
76: iconst_0
77: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
// ----System.out.println(e == f);
80: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
83: aload_3
// ----調用實例方法 Integer.intValue
84: invokevirtual #8 // Method java/lang/Integer.intValue:()I
87: aload_1
// ----調用實例方法 Integer.intValue
88: invokevirtual #8 // Method java/lang/Integer.intValue:()I
91: aload_2
92: invokevirtual #8 // Method java/lang/Integer.intValue:()I
95: iadd
96: if_icmpne 103
99: iconst_1
100: goto 104
103: iconst_0
104: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
// ----83 - 104 對應於 System.out.println(c == (a + b)); => System.out.println(c.intValue() == (a.intValue() + b.intValue())); 發生了自動拆箱
107: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
110: aload_3
111: aload_1
112: invokevirtual #8 // Method java/lang/Integer.intValue:()I
115: aload_2
116: invokevirtual #8 // Method java/lang/Integer.intValue:()I
119: iadd
120: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
123: invokevirtual #9 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
126: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
// ----107 - 126 對應於 System.out.println(c.equals(a + b)); => System.out.println(c.equals(Integer.valueOf(a.intValue() + b.intValue()))); 發生了自動拆箱與自動裝箱
129: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
132: aload 7
134: invokevirtual #10 // Method java/lang/Long.longValue:()J
137: aload_1
138: invokevirtual #8 // Method java/lang/Integer.intValue:()I
141: aload_2
142: invokevirtual #8 // Method java/lang/Integer.intValue:()I
145: iadd
// ----將棧頂int型數值強制轉換成long型數值並將結果壓入棧頂
146: i2l
147: lcmp
148: ifne 155
151: iconst_1
152: goto 156
155: iconst_0
156: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
// ----129 - 156 對應於 System.out.println(g == (a + b)); => System.out.println(g.longValue() == (a.intValue() + b.intValue())); 發生了自動拆箱和一次強制轉型
159: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
162: aload 7
164: aload_1
165: invokevirtual #8 // Method java/lang/Integer.intValue:()I
168: aload_2
169: invokevirtual #8 // Method java/lang/Integer.intValue:()I
172: iadd
173: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
176: invokevirtual #11 // Method java/lang/Long.equals:(Ljava/lang/Object;)Z
179: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
// ----159 - 179 對應於 System.out.println(g.equals(a + b)); => System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue())));
182: return
}
所以解除語法糖之後的代碼是這樣子的:
/* 根據反編譯後的字節碼反推 Java 代碼 */
public class Autoboxing {
public static void main(String[] args) {
Integer a = Integer.valueOf(1); // Integer a = 1;
Integer b = Integer.valueOf(2); // Integer b = 2;
Integer c = Integer.valueOf(3); // Integer c = 3;
Integer d = Integer.valueOf(3); // Integer d = 3;
Integer e = Integer.valueOf(321); // Integer e = 321;
Integer f = Integer.valueOf(321); // Integer f = 321;
Long g = Long.valueOf(3); // Long g = 3L;
System.out.println(c == d); // System.out.println(c == d);
System.out.println(e == f); // System.out.println(e == f);
System.out.println(c.intValue() == (a.intValue() + b.intValue())); // System.out.println(c == (a + b));
System.out.println(c.equals(Integer.valueOf(a.intValue() + b.intValue())));// System.out.println(c.equals(a + b));
System.out.println(g.longValue() == (a.intValue() + b.intValue())); // System.out.println(g == (a + b));
System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue())));// System.out.println(g.equals(a + b));
}
}
輸出結果爲:
output:
true
false
true
true
true
false
咦?爲什麼 c == d 爲 true 而 e == f 爲 false 呢?
原因在這:
static final int low = -128;
static final int high;// 可通過 property 文件配置,默認值爲 127
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到,Integer 類做了一個緩存,[-128, 127] 之間的值都被緩存起來了。所以 兩個 Integer.valueOf(3) 表示的是同一個對象,而 Integer.valueOf(321) 是兩個不同的對象,因此,一個是 true ,一個是 false。
這裏需要注意的是:包裝器類型的 “==” 運算在不遇到算術運算的情況下不會自動拆箱,比較的是引用,並且 equals() 方法不會處理數據轉型的問題。實際編碼中應當儘量避免這些情況。
3、遍歷循環
遍歷循環需要被遍歷的對象實現 Iterable 接口,因爲其本質是調用底層的迭代器。例如:
/* 編譯前 Java 代碼 */
public class ForeachSyntacticSugar {
public static void main(String[] args) {
List names = Arrays.asList(1, "sd");
for (Object name : names) {
System.out.println(name);
}
}
}
編譯成 Class 文件爲:
/* 編譯後 Class 文件 */
public class ForeachSyntacticSugar {
public ForeachSyntacticSugar() {
}
public static void main(String[] var0) {
List var1 = Arrays.asList(1, "sd");
Iterator var2 = var1.iterator();
while(var2.hasNext()) {
Object var3 = var2.next();
System.out.println(var3);
}
}
}
4、變長參數
變長,即參數的個數不定,可以是任意個。
/* 編譯前 Java 代碼 */
public class VariableParameter {
public void variable_parameter_test(Object... args) {
for (Object arg : args) {
System.out.println(arg);
}
}
}
編譯成 Class 文件:
/* 編譯後 Class 文件 */
public class VariableParameter {
public VariableParameter() {
}
public void variable_parameter_test(Object... var1) {
Object[] var2 = var1;
int var3 = var1.length;
for(int var4 = 0; var4 < var3; ++var4) {
Object var5 = var2[var4];
System.out.println(var5);
}
}
}
可以看到,Java 中的變長參數底層其實是一個數組。
使用變長參數時的注意事項:
(1)使用了變長參數的方法也可以重載,但重載可能導致歧義。
(2)在 JDK 5之前,可變長度參數可以用兩種方式處理:一種是使用重載,而另一種是使用數組參數。
(3)方法中只能有一個變量參數,且必須是最後一個參數。
5、條件編譯
首先,產生條件編譯的情況是條件爲常量的 If 語句,編譯器會根據布爾常量值的真假將分支中不成立的代碼塊消除掉。例如:
/* 編譯前 Java 代碼 */
public class ConditionCompile {
public static void main(String[] args) {
if (true) {
System.out.println("it's true!");
} else {
System.out.println("it's false!");
}
}
}
在編譯後是這樣子的:
/* 編譯後 Class 文件 */
public class ConditionCompile {
public ConditionCompile() {
}
public static void main(String[] var0) {
System.out.println("it's true!");
}
}
當然,正常情況下,我們是不會傻到寫出這種代碼的,知道有這回事就行了。
6、枚舉類
舉一個最簡單的枚舉的例子:
/* 編譯前 Java 代碼 */
public enum HumanEnum {
MAN, WOMAN
}
通過 javac HumanEnum.java
得到的是這樣子的:
/* 編譯後 Class 文件 */
public enum HumanEnum {
MAN,
WOMAN;
private HumanEnum() {
}
}
看不出來到底發生了什麼,所以我們通過 javap -c HumanEnum.class
查看字節碼:
/* 反編譯 Java 字節碼 */
public final class com.yhh.example.syntactic.sugar.HumanEnum extends java.lang.Enum<com.yhh.example.syntactic.sugar.HumanEnum> {
public static final com.yhh.example.syntactic.sugar.HumanEnum MAN;
public static final com.yhh.example.syntactic.sugar.HumanEnum WOMAN;
public static com.yhh.example.syntactic.sugar.HumanEnum[] values();
Code:
0: getstatic #1 // Field $VALUES:[Lcom/yhh/example/syntactic/sugar/HumanEnum;
3: invokevirtual #2 // Method "[Lcom/yhh/example/syntactic/sugar/HumanEnum;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Lcom/yhh/example/syntactic/sugar/HumanEnum;"
9: areturn
public static com.yhh.example.syntactic.sugar.HumanEnum valueOf(java.lang.String);
Code:
0: ldc #4 // class com/yhh/example/syntactic/sugar/HumanEnum
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class com/yhh/example/syntactic/sugar/HumanEnum
9: areturn
static {};
Code:
0: new #4 // class com/yhh/example/syntactic/sugar/HumanEnum
3: dup
4: ldc #7 // String MAN
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field MAN:Lcom/yhh/example/syntactic/sugar/HumanEnum;
13: new #4 // class com/yhh/example/syntactic/sugar/HumanEnum
16: dup
17: ldc #10 // String WOMAN
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field WOMAN:Lcom/yhh/example/syntactic/sugar/HumanEnum;
26: iconst_2
27: anewarray #4 // class com/yhh/example/syntactic/sugar/HumanEnum
30: dup
31: iconst_0
32: getstatic #9 // Field MAN:Lcom/yhh/example/syntactic/sugar/HumanEnum;
35: aastore
36: dup
37: iconst_1
38: getstatic #11 // Field WOMAN:Lcom/yhh/example/syntactic/sugar/HumanEnum;
41: aastore
42: putstatic #1 // Field $VALUES:[Lcom/yhh/example/syntactic/sugar/HumanEnum;
45: return
}
通過上述字節碼,我們大致可以還原出 HumanEnum 的普通類:
/* 根據反編譯後的字節碼反推 Java 代碼 */
public final class HumanEnum extends Enum<HumanEnum> {
public static final HumanEnum MAN;
public static final HumanEnum HUMAN;
private static final HumanEnum $VALUES[];
static {
MAN = new HumanEnum("MAN", 0);
HUMAN = new HumanEnum("HUMAN", 1);
$VALUES = (new HumanEnum[]{
MAN, HUMAN
});
}
private HumanEnum(String name, int original) {
super(name, original);
}
public static HumanEnum[] values() {
return (HumanEnum[]) $VALUES.clone();
}
public static HumanEnum valueOf(String name) {
return (HumanEnum) Enum.valueOf(HumanEnum.class, name);
}
}
當然上面的那個類是無法被編譯的,因爲 Java 編譯器限制了我們顯式的繼承自 java.Lang.Enum 類, 報錯 Classes cannot directly extends 'java.lang.Enum'
。
7、內部類
/* 編譯前 Java 代碼 */
public class InnerClass {
class Foo {
private String name;
public Foo(String name) {
this.name = name;
}
}
}
這是最簡單的一個成員內部類,編譯之後發現新增了兩個文件:InnerClass.class 和 InnerClass$Foo.class,通過 javap -c src/main/java/com/yhh/example/syntactic/sugar/InnerClass
命令得到外部類的字節碼如下:
/* 反編譯 Java 字節碼 */
public class com.yhh.example.syntactic.sugar.InnerClass {
public com.yhh.example.syntactic.sugar.InnerClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
}
咦,從這字節碼來看,其內部類的信息全不見了。
然後我們再看 InnerClass符號前加一個轉義字符,不然編譯得到的還是 InnerClass 類的字節碼。
javap -c src/main/java/com/yhh/example/syntactic/sugar/InnerClass$Foo.class` )
/* 反編譯 Java 字節碼 */
class com.yhh.example.syntactic.sugar.InnerClass$Foo {
final com.yhh.example.syntactic.sugar.InnerClass this$0;
public com.yhh.example.syntactic.sugar.InnerClass$Foo(com.yhh.example.syntactic.sugar.InnerClass, java.lang.String);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/yhh/example/syntactic/sugar/InnerClass;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: aload_0
10: aload_2
11: putfield #3 // Field name:Ljava/lang/String;
14: return
}
咦,發現內部類 Foo 中多了一個聲明爲 final 的外部類類型的引用 this$0,並且在 Foo 的構造函數中完成了初始化。看到這個,對於成員內部類爲什麼能訪問其外部類所有屬性(包括 private)、並且依附其外部類而存在等特點也就統統想明白了。
編譯之後外部類和內部類是完全獨立的 Class 文件,外部類中不包含任何內部類的信息,而所有的內部類都擁有一個外部類對象的引用。
除了成員內部類之外,還有靜態內部類、局部內部類和匿名內部類,不過它們的原理都是類似的。
8、try 語句中定義和關閉資源
JDK 1.7 之後才支持。在 JDK 1.7 以前,去操作系統資源時,比如流、文件或者 Socket 連接等,需要手動的去關閉資源。而現在,只需要這樣:
/* 編譯前 Java 代碼 */
public class WithResourse {
public void with_resourse_test() {
try (OutputStream os = new FileOutputStream("filePath")) {
os.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然後就不用擔心中途拋異常導致資源無法關閉,造成資源泄漏,編譯器會幫我們做好關閉資源的操作,是不是簡單多了?至於是怎麼做的呢,請看 Class 文件:
/* 編譯後 Class 文件 */
public class WithResourse {
public WithResourse() {
}
public void with_resourse_test() {
try {
FileOutputStream var1 = new FileOutputStream("filePath");
Throwable var2 = null;
try {
var1.flush();
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (var1 != null) {
if (var2 != null) {
try {
var1.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
var1.close();
}
}
}
} catch (IOException var14) {
var14.printStackTrace();
}
}
}
仔細一看,咦,怎麼和自己之前寫 try ... catch ... finally
是如此的類似,不過好像還多了一個操作 var2.addSuppressed(var11);
,這兒稍微提一下,這個操作是爲了避免異常屏蔽(具體可以看看 Throwable#addSuppressed 的文檔說明),在排查問題的時候會很有用。
9、結語
到此,Java 中常用的一些語法糖介紹完畢,總而言之,語法糖可以看做是編譯器實現的一些 “小把戲”,這些 “小把戲” 可能會使得效率 “大提升” ,但我們也應該去了解這些 “小把戲” 背後的真實世界,那樣才能利用好它們,而不被它們所迷惑。
參考資料:
(1)《深入理解 Java 虛擬機》周志明 著.