Java非靜態內部類外部this物件和final型別詳解
1. 非靜態內部類是如何引用外部類this物件的
Java內部類分為靜態內部類和非靜態內部類。它們有一個比較大的區別在於,非靜態內部類擁有外部類的this物件的引用,從而使得非靜態內部類可以訪問外部類的成員函式,成員變數。這個結論我們大家都比較清楚,那麼原理大家都懂嗎?這篇文章我講通過反編譯的方法一探其中的奧祕
public class OuterClass {
public void test() {
System.out.println("test");
}
class InnerClass {
public void test() {
OuterClass.this.test();
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
innerClass.test();
}
}
InnerClass的test()方法通過OuterClass.this物件直接呼叫外部類的test()方法。那麼OuterClass.this物件到底是什麼,它又是怎麼初始化到InnerClass物件裡的呢。下面通過檢視class位元組碼指令來一探究竟
在classes目錄中我們可以看到分別生成了兩個class檔案 分別為 OuterClass InnerClass.class
Compiled from "OuterClass.java"
class com.peter.tips.collections.OuterClass$InnerClass {
final com.peter.tips.collections.OuterClass this$0;
com.peter.tips.collections.OuterClass$InnerClass(com.peter.tips.collections.OuterClass);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/peter/tips/collections/OuterClass;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
public void test();
Code:
0: aload_0
1: getfield #1 // Field this$0:Lcom/peter/tips/collections/OuterClass;
4: pop
5: invokestatic #3 // Method com/peter/tips/collections/OuterClass.test:()V
8: return
}
通過反編譯InnerClass的位元組碼。我們發現InnerClass多了一個成員變數this 0是如何被賦值的? 2.this$0是否就是OuterClass.this物件呢?
1. this$0賦值過程
- 我們來觀察下InnerClass的建構函式OuterClass$InnerClass(com.peter.tips.collections.OuterClass)。位元組碼
//獲取建構函式的引數OuterClass
1: aload_1
//賦值給this$0物件
2: putfield #1 // Field this$0:Lcom/peter/tips/collections/OuterClass;
正是通過傳入的OuterClass物件給this$0賦值。
- 反編譯OuterClass的main方法,我們看看outerClass.new InnerClass()這行程式碼做了什麼
public static void main(java.lang.String[]);
Code:
//建立OuterClass物件
0: new #5 // class com/peter/tips/collections/OuterClass
//物件再次壓入棧
3: dup
//初始化OuterClass
4: invokespecial #6 // Method "<init>":()V
//OuterClass物件賦值給outerClass變數
7: astore_1
//建立InnerClass物件
8: new #7 // class com/peter/tips/collections/OuterClass$InnerClass
//物件再次壓入棧
11: dup
//outClass物件壓入棧
12: aload_1
//outClass物件壓入棧
13: dup
//呼叫getClass()方法
14: invokevirtual #8 // Method java/lang/Object.getClass:()Ljava/lang/Class;
//出棧
17: pop
//初始化InnerClass 相當於 new InnerClass(outerClass)
18: invokespecial #9 // Method com/peter/tips/collections/OuterClass$InnerClass."<init>":(Lcom/peter/tips/collections/OuterClass;)V
//把建立的InnerClass物件賦值給innerClass變數 innerClass = new InnerClass(outerClass)
21: astore_2
//innerClass物件入棧
22: aload_2
//呼叫innerClass的test()方法
23: invokevirtual #10 // Method com/peter/tips/collections/OuterClass$InnerClass.test:()V
26: return
綜上,我們知道建立內部類物件,下面兩個程式碼塊是等價的
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
OuterClass outerClass = new OuterClass();
InnerClass innerClass = new InnerClass(outerClass);
2. this$0是否就是OuterClass.this物件
前面可以知道this$0物件其實就是新建的OuterClass物件,大膽的猜測下結果,因為程式碼 只建立了一個OuterClass物件,他們指向的肯定是同一個物件了。通過檢視InnerClass的test()位元組碼也可以佐證這個結論
InnerClass
public void test();
Code:
//InnerClass物件入棧
0: aload_0
//獲取到this$0物件
1: getfield #1 // Field this$0:Lcom/peter/tips/collections/OuterClass;
//呼叫this$0的test()方法 == OuterClass.this.test()
4: invokevirtual #3 // Method com/peter/tips/collections/OuterClass.test:()V
7: return
當然我們還可以通過反射來證明OuterClass.this、this$0的存在
System.out.println(innerClass.getClass().getDeclaredField("this$0"));
final com.peter.tips.nest.OuterClass com.peter.tips.nest.OuterClass$InnerClass.this$0
2. 匿名內部類使用外部引數為什麼要用final
我們都知道如果在方法內建立匿名內部類,如果在匿名內部類中使用了方法的引數,或者區域性變數。它們需要被定義成final型別。這是為什麼呢?我們來看以下程式碼
public class Anonymous {
public void test(final int i,final String str){
String j ="hello world";
new InnerClass(){
@Override
void run() {
System.out.println("j="+j+";i="+i+";str="+str);
}
}.run();
// j="Hi world";
}
public static void main(String[] args) {
Anonymous anonymous = new Anonymous();
anonymous.test(1,"hello");
}
class InnerClass{
void run(){
}
}
}
前面OuterClass的InnerClass會預設建立一個this 1.class、Anonymous 1.class正是Anonymous的test()方法中建立的匿名內部類物件。
Compiled from "Anonymous.java"
class com.peter.tips.nest.Anonymous$1 extends com.peter.tips.nest.Anonymous$InnerClass {
final java.lang.String val$j;
final int val$i;
final java.lang.String val$str;
final com.peter.tips.nest.Anonymous this$0;
com.peter.tips.nest.Anonymous$1(com.peter.tips.nest.Anonymous, java.lang.String, int, java.lang.String);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/peter/tips/nest/Anonymous;
5: aload_0
6: aload_2
7: putfield #2 // Field val$j:Ljava/lang/String;
10: aload_0
11: iload_3
12: putfield #3 // Field val$i:I
15: aload_0
16: aload 4
18: putfield #4 // Field val$str:Ljava/lang/String;
21: aload_0
22: aload_1
23: invokespecial #5 // Method com/peter/tips/nest/Anonymous$InnerClass."<init>":(Lcom/peter/tips/nest/Anonymous;)V
26: return
void run();
Code:
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #7 // class java/lang/StringBuilder
6: dup
7: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
10: ldc #9 // String j=
12: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: getfield #2 // Field val$j:Ljava/lang/String;
19: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: ldc #11 // String ;i=
24: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
27: aload_0
28: getfield #3 // Field val$i:I
31: invokevirtual #12 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
34: ldc #13 // String ;str=
36: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: aload_0
40: getfield #4 // Field val$str:Ljava/lang/String;
43: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
46: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
49: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
52: return
}
通過檢視位元組碼我們可以看到,不管是形參還是區域性變數,最終都被傳到建構函式的形參裡去了。而且Anonymous$1成員變數定義的都是final型別。所以外部引數也需要是final的才行
3. 非靜態內部類是如何導致記憶體洩漏的
public class MemoryLeak {
public static void main(String[] args) {
MemoryLeak memoryLeak = new MemoryLeak();
InnerClass innerClass = memoryLeak.new InnerClass();
memoryLeak = null;
System.gc();
}
class InnerClass {
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize");
}
}
我們預期的程式將會列印”finalize”。但是並沒有。原因是內部類物件持有了外部類物件的引用導致無法會回收