JVM-編譯期處理(語法糖)

編譯期處理(語法糖)

所謂的 語法糖 ,其實就是指 java 編譯器把 *.java 源碼編譯爲 *.class 字節碼的過程中,自動生成 和轉換的一些代碼,主要是爲了減輕程序員的負擔,算是 java 編譯器給我們的一個額外福利(給糖吃嘛)

注意,以下代碼的分析,藉助了 javap 工具,idea 的反編譯功能,idea 插件 jclasslib 等工具。另外, 編譯器轉換的結果直接就是 class 字節碼,只是爲了便於閱讀,給出了 幾乎等價 的 java 源碼方式,並不是編譯器還會轉換出中間的 java 源碼,切記

1 默認構造器

public class Candy1 { }

編譯成class後的代碼:

public class Candy1 { 
	// 這個無參構造是編譯器幫助我們加上的 
		public Candy1() { 
				super(); // 即調用父類 Object 的無參構造方法,即調用 java/lang/Object." <init>":()V 
		} 
}

2 自動拆裝箱

這個特性是 JDK 5 開始加入的, 代碼片段1 :

public class Candy2 { 
	public static void main(String[] args) { 
		Integer x = 1; 
		int y = x; 
	} 
}

這段代碼在 JDK 5 之前是無法編譯通過的,必須改寫爲 代碼片段 2 :

public class Candy2 { 
	public static void main(String[] args) { 
		Integer x = Integer.valueOf(1); 
		int y = x.intValue(); 
	} 
}
顯然之前版本的代碼太麻煩了,需要在基本類型和包裝類型之間來回轉換(尤其是集合類中操作的都是 包裝類型),因此這些轉換的事情在 JDK 5 以後都由編譯器在編譯階段完成。即 `代碼片段1` 都會在編 譯階段被轉換爲 `代碼片段2`  

3 泛型集合取值

泛型也是在 JDK 5 開始加入的特性,但 java 在編譯泛型代碼後會執行 泛型擦除 的動作,即泛型信息 在編譯爲字節碼之後就丟失了,實際的類型都當做了 Object 類型來處理:

public class Candy3 {

	public static void main(String[] args) { 
  	List<Integer> list = new ArrayList<>(); 
 	  list.add(10); // 實際調用的是 List.add(Object e) 
 	  Integer x = list.get(0); // 實際調用的是 Object obj = List.get(int index); 
  }
}

所以在取值時,編譯器真正生成的字節碼中,還要額外做一個類型轉換的操作:

// 需要將 Object 轉爲 Integer 
Integer x = (Integer)list.get(0);

如果前面的

x 變量類型修改爲 int 基本類型那麼最終生成的字節碼是:

// 需要將 Object 轉爲 Integer, 並執行拆箱操作 
int x = ((Integer)list.get(0)).intValue();

還好這些麻煩事都不用自己做。

擦除的是字節碼上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法參數泛型的信息

 public cn.itcast.jvm.t3.candy.Candy3();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/candy/Candy3;
public static void main(java.lang.String[]); 
	descriptor: ([Ljava/lang/String;)V 
  flags: ACC_PUBLIC, ACC_STATIC 
  Code:
		stack=2, locals=3, args_size=1 
       -------new ArrayList對象----------         
       0: new #2   // class java/util/ArrayList 
       3: dup 
       4: invokespecial #3 // Method java/util/ArrayList."<init>":()V 
       7: astore_1 
                
       8: aload_1 
       9: bipush 10 
       11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 
       //add方法的參數類型實際是Object類型
       14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 
       19: pop 
       20: aload_1 
       21: iconst_0 
       //get方法的參數類型實際是Object類型
       22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 
       //	強制類型轉換         
       27: checkcast #7 // class java/lang/Integer 
       30: astore_2 
       31: return 
       LineNumberTable:
					line 8: 0 
          line 9: 8 
          line 10: 20 
          line 11: 31 
       LocalVariableTable:
			  	Start Length Slot Name    Signature
              0   32    0   args      [Ljava/lang/String; 
              8   24    1   list      Ljava/util/List;
       LocalVariableTypeTable:
					Start Length  Slot   Signature
					    8   24     1      Ljava/util/List<Ljava/lang/Integer;>;
  • list.add 和 get 方法在bycecode層面上的參數類型都是Object類型,最後會執行checkcast指令進行類型轉換爲真實類型

  • 方法體內部的泛型在虛擬機執行時會進行泛型擦除

  • 使用反射,能夠獲得方法參數和返回值中的泛型信息:

public Set<Integer> test(List<String> list, Map<Integer, Object> map) { }

Method test = Candy3.class.getMethod("test", List.class, Map.class);
        Type[] types = test.getGenericParameterTypes();
        for (Type type : types) {
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                System.out.println("原始類型 - " + parameterizedType.getRawType());
                Type[] arguments = parameterizedType.getActualTypeArguments();
                for (int i = 0; i < arguments.length; i++) {
                    System.out.printf("泛型參數[%d] - %s\n", i, arguments[i]);
                }
            }
				}

輸出

原始類型 - interface java.util.List 
泛型參數[0] - class java.lang.String 
原始類型 - interface java.util.Map 
泛型參數[0] - class java.lang.Integer 
泛型參數[1] - class java.lang.Object

4 可變參數

可變參數也是 JDK 5 開始加入的新特性: 例如:

public class Candy4 {

    public static void foo(String... args) {
        String[] array = args;
        // 直接賦值 
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo("hello", "world");
    }
}

可變參數 String... args 其實是一個 String[] args ,從代碼中的賦值語句中就可以看出來。 同 樣 java 編譯器會在編譯期間將上述代碼變換爲:

public class Candy4 {
    public static void foo(String[] args) {
        String[] array = args;
        // 直接賦值 
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo(new String[]{"hello", "world"});
    }
}

注意 如果調用了 foo() 則等價代碼爲 foo(new String[]{}) ,創建了一個空的數組,而不會 傳遞 null 進去

5 foreach 循環

仍是 JDK 5 開始引入的語法糖,數組的循環:

public class Candy5_1 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        // 數組賦初值的簡化寫法也是語法糖哦 
        for (int e : array) {
            System.out.println(e);
        }
    }
} 

會被編譯器轉換爲:

public class Candy5_1 {
    public Candy5_1() {}
    public static void main(String[] args) {
        int[] array = new int[]{1, 2, 3, 4, 5};
        for (int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e);
        }
    }
}

而集合的循環:

public class Candy5_2 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}

實際被編譯器轉換爲對迭代器的調用:

public class Candy5_2 {
    public Candy5_2() {}
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        Iterator iter = list.iterator();
        while (iter.hasNext()) {
            Integer e = (Integer) iter.next();
            System.out.println(e);
        }
    }
}

注意 foreach 循環寫法,能夠配合數組,以及所有實現了 Iterable 接口的集合類一起使用,其 中 Iterable 用來獲取集合的迭代器( Iterator

6 switch 字符串

從 JDK 7 開始,switch 可以作用於字符串和枚舉類,這個功能其實也是語法糖,例如:

public class Candy6_1 {
    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }
}

注意

switch 配合 String 和枚舉使用時,變量不能爲null,原因分析完語法糖轉換後的代碼應當自 然清楚

會被編譯器轉換爲:

public class Candy6_1 {
    public Candy6_1() { }
    public static void choose(String str) {
        byte x = -1;
        switch (str.hashCode()) {
            case 99162322: // hello 的 hashCode 
                if (str.equals("hello")) {
                    x = 0;
                }
                break;
            case 113318802: // world 的 hashCode 
                if (str.equals("world")) {
                    x = 1;
                }
        }
        switch (x) {
            case 0:
                System.out.println("h");
                break;
            case 1:
							  System.out.println("w");
        }
    }
}

可以看到,執行了兩遍 switch,第一遍是根據字符串的 hashCodeequals 將字符串的轉換爲相應 byte 類型,第二遍纔是利用 byte 執行進行比較。

爲什麼第一遍時必須既比較 hashCode,又利用 equals 比較呢?hashCode 是爲了提高效率,減少可 能的比較;而 equals 是爲了防止 hashCode 衝突,例如 BMC. 這兩個字符串的hashCode值都是 2123 ,如果有如下代碼:

public class Candy6_2 {
    public static void choose(String str) {
        switch (str) {
            case "BM": {
                System.out.println("h");
                break;
            }
            case "C.": {
                System.out.println("w");
                break;
            }
        }
    }
}

會被編譯器轉換爲:

public class Candy6_2 {
    public Candy6_2() { }
    public static void choose(String str) {
        byte x = -1;
        switch (str.hashCode()) {
            case 2123: // hashCode 值可能相同,需要進一步用 equals 比較 
                if (str.equals("C.")) {
                    x = 1;
                } else if (str.equals("BM")) {
                    x = 0;
                }
            default:
                switch (x) {
                    case 0:
                        System.out.println("h");
                        break;
                    case 1:
                        System.out.println("w");
                }
        }
    }
}

7 switch 枚舉

switch 枚舉的例子,原始代碼:

enum Sex { 
  MALE, FEMALE 
}
public class Candy7 {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println("男"); break;
            case FEMALE:
                System.out.println("女"); break;
        }
    }
}

轉換後代碼:

public class Candy7 {
    /**
     * 定義一個合成類,$MAP這個名字自己取的,jvm中不是這樣(僅 jvm 使用,對我們不可見) 
     * 用來映射枚舉的 ordinal 與數組元素的關係 
     * 枚舉的 ordinal 表示枚舉對象的序號,從 0 開始 
     * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
     */
    static class $MAP {

        // 數組大小即爲枚舉元素個數,裏面存儲case用來對比的數字
        static int[] map = new int[2];
        static {
            map[Sex.MALE.ordinal()] = 1;
            map[Sex.FEMALE.ordinal()] = 2;
        }
    }
    public static void foo(Sex sex) {
        int x = $MAP.map[sex.ordinal()];
        switch (x) {
            case 1:
                System.out.println("男");
                break;
            case 2:
                System.out.println("女");
                break;
        }
    }
}

8 枚舉類

以前面的性別枚舉爲例:

enum Sex { 
  MALE, FEMALE 
}

轉換後代碼:

public final class Sex extends Enum<Sex> {
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;

    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    }

    /**
     * Sole constructor. Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     * 
     * @param name - The name of this enum constant, which is the identifier  used to declare it.
     *
     * @param ordinal - The ordinal of this enumeration constant (its position in the enum declaration, where the initial constant is assigned
     */
    private Sex(String name, int ordinal) {
        super(name, ordinal);
    }
    public static Sex[] values() {
        return $VALUES.clone();
    }
    public static Sex valueOf(String name) {
        return Enum.valueOf(Sex.class, name);
    }
}

9 匿名內部類

源代碼:

public class Candy11 {

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        };
    }
}

轉換後代碼:

// 額外生成的類 
final class Candy11$1 implements Runnable {
    Candy11$1() { }
    public void run() {
        System.out.println("ok");
    }

}
public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Candy11$1();
    }
}

引用局部變量的匿名內部類,源代碼:

public class Candy11 {

    public static void test(final int x) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok:" + x);
            }
        };
    }
}

轉換後代碼:

// 額外生成的類
final class Candy11$1 implements Runnable {
    int val$x;

    Candy11$1(int x) {
        this.val$x = x;
    }
    public void run() {
        System.out.println("ok:" + this.val$x);
    }

}
public class Candy11 {
    public static void test(final int x) {
        Runnable runnable = new Candy11$1(x);
    }
}

注意 這同時解釋了爲什麼匿名內部類引用局部變量時,局部變量必須是 final 的:因爲在創建 Candy11$1 對象時,將 x 的值賦值給了 Candy11$1 對象的 val$x 屬 性 ,所以x 不應該再發生變化了,如果 變化 , 那 麼 val$x 屬性沒有機會再跟着一起變化 .

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