天天看點

内部類引用局部變量與外部類成員變量的問題思考

昨天有一個比較愛思考的同僚和我提起一個問題:為什麼匿名内部類使用的局部變量和參數需要final修飾,而外部類的成員變量則不用?對這個問題我一直作為預設的文法了,木有仔細想過為什麼(在分析完後有點印象在哪本書上看到過,但是就是沒有找到,難道是我的幻覺?呵呵)。雖然沒有想過,但是還是借着之前研究過位元組碼的基礎上,分析了一些,感覺上是找到了一些答案,分享一下;也希望有大牛給指出一些不足的地方。

  假如我們有以下的代碼:

interface Printer { 

    public void print(); 

class MyApplication { 

    private int field = 10; 

     public void print(final Integer param) { 

        final long local = 100; 

        final long local2 = param.longValue() + 100; 

        Printer printer = new Printer() { 

            @Override 

            public void print() { 

                System.out.println("Local value: " + local); 

                System.out.println("Local2 value: " + local2); 

                System.out.println("Parameter: " + param); 

                System.out.println("Field value: " + field); 

            } 

        }; 

        printer.print(); 

    } 

}

  這裡因為param要在匿名内部類的print()方法中使用,因而它要用final修飾;local/local2是局部變量,因而也需要final修飾;而field是外部類MyApplication的字段,因而不需要final修飾。這種設計是基于什麼理由呢?

  1、匿名内部類可以使用外部類的變量(局部或成員變來那個)。

  2、匿名内部類中不同的方法可以共享這些變量。

  根據這兩點資訊我們就可以分析,可能這些變量會在匿名内部類的字段中儲存着,并且在構造的時候将他們的值/引用傳入内部類。這樣就可以保證同時實作上述兩點了。

  事實上,Java就是這樣設計的,并且所謂匿名類,其實并不是匿名的,隻是編譯器幫我們命名了而已。這點我們可以通過這兩個類編譯出來的位元組碼看出來:

// Compiled from Printer.java (version 1.6 : 50.0, super bit) 

class levin.test.anonymous.MyApplication$1 implements levin.test.anonymous.Printer { 

  // Field descriptor #8 Llevin/test/anonymous/MyApplication; 

  final synthetic levin.test.anonymous.MyApplication this$0; 

  // Field descriptor #10 J 

  private final synthetic long val$local2; 

  // Field descriptor #12 Ljava/lang/Integer; 

  private final synthetic java.lang.Integer val$param; 

  // Method descriptor #14 (Llevin/test/anonymous/MyApplication;JLjava/lang/Integer;)V 

  // Stack: 3, Locals: 5 

  MyApplication$1(levin.test.anonymous.MyApplication arg0, long arg1, java.lang.Integer arg2); 

     0  aload_0 [this] 

     1  aload_1 [arg0] 

     2  putfield levin.test.anonymous.MyApplication$1.this$0 : levin.test.anonymous.MyApplication [16] 

     5  aload_0 [this] 

     6  lload_2 [arg1] 

     7  putfield levin.test.anonymous.MyApplication$1.val$local2 : long [18] 

    10  aload_0 [this] 

    11  aload 4 [arg2] 

    13  putfield levin.test.anonymous.MyApplication$1.val$param : java.lang.Integer [20] 

    16  aload_0 [this] 

    17  invokespecial java.lang.Object() [22] 

    20  return 

      Line numbers: 

        [pc: 0, line: 1] 

        [pc: 16, line: 13] 

      Local variable table: 

        [pc: 0, pc: 21] local: this index: 0 type: new levin.test.anonymous.MyApplication(){} 

  // Method descriptor #24 ()V 

  // Stack: 4, Locals: 1 

  public void print(); 

     0  getstatic java.lang.System.out : java.io.PrintStream [30] 

     3  ldc <String "Local value: 100"> [36] 

     5  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38] 

     8  getstatic java.lang.System.out : java.io.PrintStream [30] 

    11  new java.lang.StringBuilder [44] 

    14  dup 

    15  ldc <String "Local2 value: "> [46] 

    17  invokespecial java.lang.StringBuilder(java.lang.String) [48] 

    20  aload_0 [this] 

    21  getfield levin.test.anonymous.MyApplication$1.val$local2 : long [18] 

    24  invokevirtual java.lang.StringBuilder.append(long) : java.lang.StringBuilder [50] 

    27  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54] 

    30  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38] 

    33  getstatic java.lang.System.out : java.io.PrintStream [30] 

    36  new java.lang.StringBuilder [44] 

    39  dup 

    40  ldc <String "Parameter: "> [58] 

    42  invokespecial java.lang.StringBuilder(java.lang.String) [48] 

    45  aload_0 [this] 

    46  getfield levin.test.anonymous.MyApplication$1.val$param : java.lang.Integer [20] 

    49  invokevirtual java.lang.StringBuilder.append(java.lang.Object) : java.lang.StringBuilder [60] 

    52  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54] 

    55  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38] 

    58  getstatic java.lang.System.out : java.io.PrintStream [30] 

    61  new java.lang.StringBuilder [44] 

    64  dup 

    65  ldc <String "Field value: "> [63] 

    67  invokespecial java.lang.StringBuilder(java.lang.String) [48] 

    70  aload_0 [this] 

    71  getfield levin.test.anonymous.MyApplication$1.this$0 : levin.test.anonymous.MyApplication [16] 

    74  invokestatic levin.test.anonymous.MyApplication.access$0(levin.test.anonymous.MyApplication) : int [65] 

    77  invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [71] 

    80  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54] 

    83  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38] 

    86  return 

        [pc: 0, line: 16] 

        [pc: 8, line: 17] 

        [pc: 33, line: 18] 

        [pc: 58, line: 19] 

        [pc: 86, line: 20] 

        [pc: 0, pc: 87] local: this index: 0 type: new levin.test.anonymous.MyApplication(){} 

  Inner classes: 

    [inner class info: #1 levin/test/anonymous/MyApplication$1, outer class info: #0 

     inner name: #0, accessflags: 0 default] 

  Enclosing Method: #66  #77 levin/test/anonymous/MyApplication.print(Ljava/lang/Integer;)V 

 這些字段在構造函數中指派,而構造函數則是在MyApplication.print()方法中調用。

  由此,我們可以得出一個結論:Java對匿名内部類的實作是通過編譯器來支援的,即通過編譯器幫我們産生一個匿名類的類名,将所有在匿名類中用到的局部變量和參數做為内部類的final字段,同是内部類還會引用外部類的執行個體。其實這裡少了local的變量,這是因為local是編譯器常量,編譯器對它做了替換的優化。

  其實Java中很多文法都是通過編譯器來支援的,而在虛拟機/位元組碼上并沒有什麼差別,比如這裡的final關鍵字,其實細心的人會發現在位元組碼中,param參數并沒有final修飾,而final本身的很多實作就是由編譯器支援的。類似的還有Java中得泛型和逆變、協變等。這是題外話。

  有了這個基礎後,我們就可以來分析為什麼有些要用final修飾,有些卻不用的問題。

  首先我們來分析local2變量,在”匿名類”中,它是通過構造函數傳入到”匿名類”字段中的,因為它是基本類型,因而在夠着函數中指派時(撇開對函數參數傳遞不同虛拟機的不同實作而産生的不同效果),它事實上隻是值的拷貝;因而加入我們可以在”匿名類”中得print()方法中對它指派,那麼這個指派對外部類中得local2變量不會有影響,而程式員在讀代碼中,是從上往下讀的,是以很容易誤認為這段代碼指派會對外部類中得local2變量本身産生影響,何況在源碼中他們的名字都是一樣的,是以我認為了避免這種confuse導緻的一些問題,Java設計者才設計出了這樣的文法。

  對引用類型,其實也是一樣的,因為引用的傳遞事實上也隻是傳遞引用的數值(簡單的可以了解成為位址),因而對param,如果可以在”匿名類”中指派,也不會在外部類的print()後續方法産生影響。雖然這樣,我們還是可以在内部類中改變引用内部的值的,如果引用類型不是隻讀類型的話;在這裡Integer是隻讀類型,因而我們沒法這樣做。(如果學過C++的童鞋可以想想常量指針和指針常量的差別)。

  現在還剩下最後一個問題:為什麼引用外部類的字段卻是可以不用final修飾的呢?細心的童鞋可能也已經發現答案了,因為内部類儲存了外部類的引用,因而内部類中對任何字段的修改都回真實的反應到外部類執行個體本身上,是以不需要用final來修飾它。

  這個問題基本上就分析到這裡了,不知道我有沒有表達清楚了。

  加點題外話吧。

  首先是,對這裡的位元組碼,其實還有一點可以借鑒的地方,就是内部類在使用外部類的字段時不是直接取值,而是通過編譯器在外部類中生成的靜态的access$0()方法來取值,我的了解,這裡Java設計者想盡量避免其他類直接通路一個類的資料成員,同時生成的access$0()方法還可以被其他類所使用,這遵循了面向對象設計中的兩個重要原則:封裝和複用。

  另外,對這個問題也讓我意識到了即使是語言文法層面上的設計都是有原因可循的,我們要善于多問一些為什麼,了解這些設計的原因和局限,記得曾聽到過一句話:知道一門技術的局限,我們才能很好的了解這門技術可以用來做什麼。也隻有這樣我們才能不斷的提高自己。在解決了這個問題後,我突然冒出了一句說Java這樣設計也是合理的。是啊,文法其實就一幫人建立的一種解決某些問題的方案,當然有合理和不合理之分,我們其實不用對它視若神聖。

  之前有進過某著名高校的研究所學生群,即使在那裡,碼農論也是甚嚣塵上,其實碼農不碼農并不是因為程式員這個職位引起的,而是個人引起的,我們要不斷了解代碼内部的本質才能避免一直做碼農的命運那。個人愚見而已,呵呵。

本文出自seven的測試人生公衆号最新内容請見作者的GitHub頁:http://qaseven.github.io/