本文描述在java內部類中,經常會引用外部類的變量信息。但是這些變量信息是如何傳遞給內部類的,在表面上並沒有相應的線索。本文從字節碼層描述在內部類中是如何實現這些語義的。
本地臨時變量 基本類型
final int x = 10; new Runnable() { @Override public void run() { System.out.println(x); } }.run();
當輸出內部類字節碼(javap -p -s -c -v)時,如下所示:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: bipush 10 5: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 8: return
可以看出,此常量值直接被寫在內部類的臨時變量中,即相當於進行了一次變量copy。
本地臨時變量 引用類型
final T t = new T(); new Runnable() { @Override public void run() { System.out.println(t); } }.run();
字節碼變為如下所示:
final T val$t; flags: ACC_FINAL, ACC_SYNTHETIC T$1(T); Signature: (LT;)V //構建函數的字節碼 0: aload_0 1: aload_1 2: putfield #1 // Field val$t:LT; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return //main函數字節碼 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field val$t:LT; 7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 10: return
可以看出,這時自動生成了一個帶有1個參數的構造函數,並且將相應的t值作為參數傳遞到內部類當中,同時設定final語義,即不能被內部類修改。
上面的是無參構造函數,如果是一個有參數的內部類呢,如下所示:
Thread thread = new Thread("thread-1") { @Override public void run() { System.out.println(t); } };
生成的字節碼如下:
T$1(java.lang.String, T); Signature: (Ljava/lang/String;LT;)V
可以看出,編譯器將自動對原來調用的構造函數進行了修改,將原來只需要1個參數的構造函數 修改為傳2個參數,並且同時將相應的t傳遞進去。
引用字段,基本類型
int t = 3; private void xx() { new Runnable() { @Override public void run() { System.out.println(t); } }.run(); }
生成的字節碼如下:
T$1(T); Signature: (LT;)V flags: Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LT; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return
這裡並沒有如臨時變量那樣,直接在內部類中進行常量定義。為什麼?因為這裡的t對象隨時可能被修改。
引用字段,引用類型
final String t = new String("abc"); private void xx() { new Runnable() { @Override public void run() { System.out.println(t); } }.run(); }
生成字節碼如下:
final T this$0; Signature: LT; T$1(T); //內部類構造函數 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LT; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return
這裡,在內部類的構造函數中,直接將外部類的this傳遞進來了,因此在內部類的run方法中,對於t,將直接兩層getField進行調用,即可以拿到相應的信息。如下所示:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:LT; 7: getfield #4 // Field T.t:Ljava/lang/String; 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: return
引用類型,引用類型,static字段
static String t = new String("abc"); private void xx() { new Runnable() { @Override public void run() { System.out.println(t); } }.run(); }
字節碼如下:
final T this$0; Signature: LT; flags: ACC_FINAL, ACC_SYNTHETIC T$1(T); Signature: (LT;)V //構造函數字節碼 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LT; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return //run方法字節碼 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: getstatic #4 // Field T.t:Ljava/lang/String; 6: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 9: return
可以看出,即使是引用static字段,在內部類中仍然會保留外部類的引用,即達到引用目的。同時,在run方法內部,因為是static字段,因此將不再使用getField,而是使用getStatic來進行相應字段的引用。
總結
在整個內部類字節碼的生成規則中,主要采用了修改構造函數的方式來將需要在整個內部類中引用的變量進行參數傳遞。並且,因為是內部類,構造函數是已知的,可以隨意的修改。針對特定的場景,可以進行一定的優化,如常量化(臨時變量基本類型)。
因為在整個JVM層,並沒有針對內部類作特殊的處理,因此這些處理手法都是在編譯層進行處理的。同時,在語言層,針對這些生成的信息進行指定的說明。如SYNTHETIC語義。
在反射字段Member層,定義了如下方法:
/** * Returns {@code true} if this member was introduced by * the compiler; returns {@code false} otherwise. * * @return true if and only if this member was introduced by * the compiler. * @jls 13.1 The Form of a Binary * @since 1.5 */ public boolean isSynthetic();
即此信息是由編譯器引入的。
了解這些對於整個語言層有一定的理解意義,但並不代表將來這些不會會改變,了解一些實現細節有助於自己在代碼實現層有進一步的思考空間,並不局限於之前所了解的信息。
學習Java的同學注意了!!!
學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流群,群號碼:454297367 我們一起學Java!