【JVM】-- Java編譯期處理


@


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

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

1.默認構造器

public class Candy1 {
}  

經過編譯的代碼,可以看到在編譯階段,如果我們沒有添加構造器。那么Java編譯器會為我們添加一個無參構造方法。

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

2.自動拆裝箱

在JDK5以后,Java提供了自動拆裝箱的功能。

如以下代碼:

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

在Java5以前會編譯失敗,必須該寫為以下代碼:

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

以上的轉換,在JDK5以后都會由Java編譯器自動完成。

3.泛型與類型擦除

泛型延時在JDK5以后加入的特性,但Java中的泛型並不是真正的泛型。因為Java中的泛型只存在於Java的源碼中,在經過編譯的字節碼文件中,就已經替換為原來的原生類型(RawType,也稱為裸類型,可以認為是被Object替換)了,並且在相應的地方插入了強制轉型代碼,因此,對於運行期的Java語言來說, ArrayList < int>與ArrayList< String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為類型擦除,基於這種方法實現的泛型稱為偽泛型。

如以下代碼:

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);
    }
}  

在從list集合中取值時,在編譯器真正的字節碼文件中還需要一個類型轉換的動作

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

如果前面的 x 變量類型修改為 int 基本類型那么最終生成的字節碼是:

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

不過因為語法糖的存在,所以以上的動作都不需要我們自己來做。
不過,雖然編譯器在編譯過程中,將泛型信息都擦除了,但是並不意味着,泛型信息就丟失了。泛型的信息還是會存儲在LocalVariableTypeTable 中:

{
  public wf.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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lwf/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
         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;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        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;
           31       1     2     x   Ljava/lang/Integer;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      24     1  list  Ljava/util/List<Ljava/lang/Integer;>;
}
SourceFile: "Candy3.java"

我們可以通過反射的方式獲得被擦除的泛型信息。不過只能獲取方法參數或返回值上的信息。

public class Candy3 {

    List<String> str = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        List<Integer> list = new ArrayList<>();
        list.add(10); // 實際調用的是 List.add(Object e)
        Integer x = list.get(0); // 實際調用的是 Object obj = List.get(int index);
        fs();
    }
    public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
        return null;
    }

    private static void fs() throws Exception {
        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]);
                }
            }
        }

        Field list = Candy3.class.getDeclaredField("str");
        Class<?> type = list.getType();
        System.out.println(type.getName());
    }
}

輸出:

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

4.可變參數

可變參數也是JDK5新加入的特性。其具體形式如下:

public class Test3 {

    public static void main(String[] args) {
        foo("hello","world");
    }

    private static void foo(String... args){
        String[] str = args;
        for (int i = 0; i < str.length; i++) {
            System.out.println(str[i]);
        }
    }
}

其結果由一個字符串數組直接接受,程序能夠正常執行。
注意:如果調用方法時沒有參數如foo(),那么傳入方法的不是null,而是一個空數組foo(new String[]{})。

5.foreach

依舊是JDK5引入的語法糖。簡化了for循環的寫法。

示例:

public class Test4_1 {

    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};
        for (int i : arr) {
            System.out.print(" " + i);
        }
    }
}

在對其字節碼反編譯后:

public class Test4_1 {
    public Test4_1() {
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        int[] var2 = arr;
        int var3 = arr.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            int i = var2[var4];
            System.out.print(" " + i);
        }
    }
}

此處包含兩個語法糖

  • {1,2,3,4,5}轉為數組才進行復制
  • foreach循環被轉換為了簡單的for循環。

foreach循環還可以對集合進行遍歷:

public class Test4_2 {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5);

        for (Integer integer : list) {
            System.out.print(integer + " ");
        }
    }
}

其編譯后字節碼的反編譯出的代碼為:

public class Test4_2 {
    public Test4_2() {
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            Integer integer = (Integer)var2.next();
            System.out.print(integer + " ");
        }
    }
}

編譯器先獲取集合的迭代器對象,在通過while循環對迭代器對象進行遍歷。其中還包含泛型擦除的語法糖。
foreach循環寫法,配合數組,及實現了Iterable接口的集合類使用,Iterable來獲取迭代器對象(Iterator)

6.switch支持case使用字符串及枚舉類型

JDK7開始,Java的switch支持字符串和枚舉類型,而其中也包含了語法糖。

switch字符串

示例:

public class Test5_1 {

    public static void main(String[] args) {
        switch ("hello"){
            case "hello":
                System.out.println("hello");
                break;
            case "world":
                System.out.println("world");
        }
    }
}

注意:在使用String時,不能傳入一個null,會發生空指針異常。因為以上代碼會被編譯器轉換為:

public class Test5_1 {
    public Test5_1() {
    }

    public static void main(String[] args) {
        String var1 = "hello";
        byte var2 = -1;
        switch(var1.hashCode()) {
        case 99162322:
            if (var1.equals("hello")) {
                var2 = 0;
            }
            break;
        case 113318802:
            if (var1.equals("world")) {
                var2 = 1;
            }
        }

        switch(var2) {
        case 0:
            System.out.println("hello");
            break;
        case 1:
            System.out.println("world");
        }

    }
}

可以看到,switch支持字符實際上是把對象,獲取其哈希值進行一次比較在確定了,之后再用一個switch來實現代碼邏輯。
為什么第一次既要進行一次哈希比較,又要進行一次equals()?使用hashcode是為了提高比較的效率,而equals是為了防止哈希沖突。如BM和C.兩個字符串的哈希值相同都為2123,如果有以下代碼:

public class Test5_2 {

    public static void main(String[] args) {
        switch ("BM"){
            case "BM":
                System.out.println("hello");
                break;
            case "C.":
                System.out.println("world");
        }
    }
}

經過反編譯后:

public class Test5_2 {
    public Test5_2() {
    }

    public static void main(String[] args) {
        String var1 = "BM";
        byte var2 = -1;
        switch(var1.hashCode()) {
        case 2123://哈希值相同需要進一步比較
            if (var1.equals("C.")) {
                var2 = 1;
            } else if (var1.equals("BM")) {
                var2 = 0;
            }
        default:
            switch(var2) {
            case 0:
                System.out.println("hello");
                break;
            case 1:
                System.out.println("world");
            }

        }
    }
}

switch枚舉

代碼如下:

public class Test5_3 {

    public static void main(String[] args) {
        Sex sex = Sex.MALE;

        switch (sex){
            case MALE:
                System.out.println("男");
                break;
            case FEMALE:
                System.out.println("女");
        }
    }
}

enum Sex{
    MALE,FEMALE;
}

轉換后:

/**
* 定義一個合成類(僅 jvm 使用,對我們不可見)
* 用來映射枚舉的 ordinal 與數組元素的關系
* 枚舉的 ordinal 表示枚舉對象的序號,從 0 開始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
* 該轉換需要使用其他工具進行轉換,idea轉換不出來
*/
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;
        }
    }
}  

7.枚舉

JDK7以后Java引入了枚舉類,它也是一個語法糖。

以上一個性別類型為例:

enum Sex {
    MALE, FEMALE
}

轉換后(idea依舊不能轉換):

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
    * 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);
    }
}

8.try-with-resourcs

JDK7加入對需要關閉資源處理的特殊語法。

try(資源大小 = 創建對象資源){
  
}catch(){
  
}

其中資源對象需要實現 AutoCloseable 接口,例如 InputStream 、 OutputStream 、
Connection 、 Statement 、 ResultSet 等接口都實現了 AutoCloseable ,使用 try-withresources 可以不用寫 finally 語句塊,編譯器會幫助生成關閉資源代碼,例如:

public class Test6 {

    public static void main(String[] args) {
        try(InputStream stream = new FileInputStream("F://test.txt")) {
            System.out.println(stream);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

會被轉換為;

public class Test6 {
    public Test6() {
    }

    public static void main(String[] args) {
        try {
            InputStream stream = new FileInputStream("F://test.txt");
            Throwable var2 = null;

            try {
                System.out.println(stream);
            } catch (Throwable var12) {
                //var2是可能出現的異常
                var2 = var12;
                throw var12;
            } finally {
                //判斷資源是否為空
                if (stream != null) {
                    //如果代碼出現異常
                    if (var2 != null) {
                        try {
                            stream.close();
                        } catch (Throwable var11) {
                            //關閉資源時出現異常,作為被壓制異常添加
                            var2.addSuppressed(var11);
                        }
                    } else {
                        //如果代碼沒有異常,close出現的異常就是catch中var12
                        stream.close();
                    }
                }

            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }

    }
}

為什么要設計一個 addSuppressed(Throwable e) (添加被壓制異常)的方法呢?是為了防止異常信息的丟失(想想 try-with-resources 生成的 fianlly 中如果拋出了異常):

public class Test6_1 {

    public static void main(String[] args) {
        try(Myresource myresource = new Myresource()) {
            int a = 1/0;
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

class Myresource implements AutoCloseable{

    @Override
    public void close() throws Exception {
        throw new IOException("close異常");
    }
}

其輸出為;

java.lang.ArithmeticException: / by zero
	at wf.test.Test6_1.main(Test6_1.java:9)
	Suppressed: java.io.IOException: close異常
		at wf.test.Myresource.close(Test6_1.java:20)
		at wf.test.Test6_1.main(Test6_1.java:10)

TWR將兩個異常信息都保留了下來。

9.方法重寫時的橋接方法

我們都知道,方法重寫時對返回值分兩種情況:

  • 父子類的返回值完全一致
  • 子類返回值可以是父類返回值的子類(比較繞口,見下面的例子)
class A{
    public Number m(){
        return 1;
    }
}

class B extends A{
    public Integer m(){
        return 2;
    }
}

對於子類,java 編譯器會做如下處理:

class B extends A {
    public Integer m() {
        return 2;
    } 
    // 此方法才是真正重寫了父類 public Number m() 方法
    public synthetic bridge Number m() {
    // 調用 public Integer m()
        return m();
    }
}

其中橋接方法比較特殊,僅對 java 虛擬機可見,並且與原來的 public Integer m() 沒有命名沖突,可以用下面反射代碼來驗證:

public class Test7 {

    public static void main(String[] args) {
        for (Method m :B.class.getDeclaredMethods()) {
            System.out.println(m);
        }

        A a = new B();
        System.out.println(a.m());
    }
}

輸出結果:

public java.lang.Integer wf.test.B.m()
public java.lang.Number wf.test.B.m()
2

也可以驗證該方法重寫起作用了。

10.匿名內部類

代碼:

public class Candy11 {

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

}

轉碼后:

// 額外生成的類
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();
    }
}

當匿名內部類引用外部類變量時

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

轉換后:

// 額外生成的類
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 static void test(final int x) {
    Runnable runnable = new Candy11$1(x);
}

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM