深入理解 JVM(5)熟悉語法糖背後的真相

轉載請註明原創出處,謝謝!

HappyFeet的博客

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
}

咦,從這字節碼來看,其內部類的信息全不見了。

然後我們再看 InnerClassFoo.classFoo.class 文件的字節碼: (這有一個需要注意的地方就是,需要在 `符號前加一個轉義字符,不然編譯得到的還是 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 虛擬機》周志明 著.

(2)Autoboxing and Unboxing

(3)Variable Arguments (Varargs) in Java

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章