1. 程式人生 > >Java非靜態內部類外部this物件和final型別詳解

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 I n n e

r C l a s s . c l a s s O u t e r C l a s s . c l a s s j a v a p c O u t e r C l a s s 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 1. t h i s 0是如何被賦值的? 2.this$0是否就是OuterClass.this物件呢?

1. this$0賦值過程

  1. 我們來觀察下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賦值。

  1. 反編譯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 0 A n o n y m o u s 3 A n o n y m o u s 1.class、Anonymous I n n e r C l a s s . c l a s s A n o n y m o u s . c l a s s A n o n y m o u s 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”。但是並沒有。原因是內部類物件持有了外部類物件的引用導致無法會回收