上善若水
          In general the OO style is to use a lot of little objects with a lot of little methods that give us a lot of plug points for overriding and variation. To do is to be -Nietzsche, To bei is to do -Kant, Do be do be do -Sinatra
          posts - 146,comments - 147,trackbacks - 0

          昨天有一個比較愛思考的同事和我提起一個問題:為什么匿名內(nèi)部類使用的局部變量和參數(shù)需要final修飾,而外部類的成員變量則不用?對這個問題我一直作為默認(rèn)的語法了,木有仔細(xì)想過為什么(在分析完后有點印象在哪本書上看到過,但是就是沒有找到,難道是我的幻覺?呵呵)。雖然沒有想過,但是還是借著之前研究過字節(jié)碼的基礎(chǔ)上,分析了一些,感覺上是找到了一些答案,分享一下;也希望有大牛給指出一些不足的地方。

          假如我們有以下的代碼:

           1 interface Printer {
           2     public void print();
           3 }
           4 
           5 class MyApplication {
           6     private int field = 10;
           7     
           8     public void print(final Integer param) {
           9         final long local = 100;
          10         final long local2 = param.longValue() + 100;
          11         Printer printer = new Printer() {
          12             @Override
          13             public void print() {
          14                 System.out.println("Local value: " + local);
          15                 System.out.println("Local2 value: " + local2);
          16                 System.out.println("Parameter: " + param);
          17                 System.out.println("Field value: " + field);
          18             }
          19         };
          20         printer.print();
          21     }
          22 }

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

           

          我想這個問題應(yīng)該從Java是如何實現(xiàn)匿名內(nèi)部類的。其中有兩點:
          1.
          匿名內(nèi)部類可以使用外部類的變量(局部或成員變來那個)

          2. 匿名內(nèi)部類中不同的方法可以共享這些變量

          根據(jù)這兩點信息我們就可以分析,可能這些變量會在匿名內(nèi)部類的字段中保存著,并且在構(gòu)造的時候?qū)⑺麄兊闹?/span>/引用傳入內(nèi)部類。這樣就可以保證同時實現(xiàn)上述兩點了。

           

          事實上,Java就是這樣設(shè)計的,并且所謂匿名類,其實并不是匿名的,只是編譯器幫我們命名了而已。這點我們可以通過這兩個類編譯出來的字節(jié)碼看出來:

           1 // Compiled from Printer.java (version 1.6 : 50.0, super bit)
           2 class levin.test.anonymous.MyApplication$1 implements levin.test.anonymous.Printer {
           3   
           4   // Field descriptor #8 Llevin/test/anonymous/MyApplication;
           5   final synthetic levin.test.anonymous.MyApplication this$0;
           6   
           7   // Field descriptor #10 J
           8   private final synthetic long val$local2;
           9   
          10   // Field descriptor #12 Ljava/lang/Integer;
          11   private final synthetic java.lang.Integer val$param;
          12   
          13   // Method descriptor #14 (Llevin/test/anonymous/MyApplication;JLjava/lang/Integer;)V
          14   // Stack: 3, Locals: 5
          15   MyApplication$1(levin.test.anonymous.MyApplication arg0, long arg1, java.lang.Integer arg2);
          16      0  aload_0 [this]
          17      1  aload_1 [arg0]
          18      2  putfield levin.test.anonymous.MyApplication$1.this$0 : levin.test.anonymous.MyApplication [16]
          19      5  aload_0 [this]
          20      6  lload_2 [arg1]
          21      7  putfield levin.test.anonymous.MyApplication$1.val$local2 : long [18]
          22     10  aload_0 [this]
          23     11  aload 4 [arg2]
          24     13  putfield levin.test.anonymous.MyApplication$1.val$param : java.lang.Integer [20]
          25     16  aload_0 [this]
          26     17  invokespecial java.lang.Object() [22]
          27     20  return
          28       Line numbers:
          29         [pc: 0, line: 1]
          30         [pc: 16, line: 13]
          31       Local variable table:
          32         [pc: 0, pc: 21] local: this index: 0 type: new levin.test.anonymous.MyApplication(){}
          33   
          34   // Method descriptor #24 ()V
          35   // Stack: 4, Locals: 1
          36   public void print();
          37      0  getstatic java.lang.System.out : java.io.PrintStream [30]
          38      3  ldc <String "Local value: 100"> [36]
          39      5  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38]
          40      8  getstatic java.lang.System.out : java.io.PrintStream [30]
          41     11  new java.lang.StringBuilder [44]
          42     14  dup
          43     15  ldc <String "Local2 value: "> [46]
          44     17  invokespecial java.lang.StringBuilder(java.lang.String) [48]
          45     20  aload_0 [this]
          46     21  getfield levin.test.anonymous.MyApplication$1.val$local2 : long [18]
          47     24  invokevirtual java.lang.StringBuilder.append(long) : java.lang.StringBuilder [50]
          48     27  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54]
          49     30  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38]
          50     33  getstatic java.lang.System.out : java.io.PrintStream [30]
          51     36  new java.lang.StringBuilder [44]
          52     39  dup
          53     40  ldc <String "Parameter: "> [58]
          54     42  invokespecial java.lang.StringBuilder(java.lang.String) [48]
          55     45  aload_0 [this]
          56     46  getfield levin.test.anonymous.MyApplication$1.val$param : java.lang.Integer [20]
          57     49  invokevirtual java.lang.StringBuilder.append(java.lang.Object) : java.lang.StringBuilder [60]
          58     52  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54]
          59     55  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38]
          60     58  getstatic java.lang.System.out : java.io.PrintStream [30]
          61     61  new java.lang.StringBuilder [44]
          62     64  dup
          63     65  ldc <String "Field value: "> [63]
          64     67  invokespecial java.lang.StringBuilder(java.lang.String) [48]
          65     70  aload_0 [this]
          66     71  getfield levin.test.anonymous.MyApplication$1.this$0 : levin.test.anonymous.MyApplication [16]
          67     74  invokestatic levin.test.anonymous.MyApplication.access$0(levin.test.anonymous.MyApplication) : int [65]
          68     77  invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [71]
          69     80  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54]
          70     83  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38]
          71     86  return
          72       Line numbers:
          73         [pc: 0, line: 16]
          74         [pc: 8, line: 17]
          75         [pc: 33, line: 18]
          76         [pc: 58, line: 19]
          77         [pc: 86, line: 20]
          78       Local variable table:
          79         [pc: 0, pc: 87] local: this index: 0 type: new levin.test.anonymous.MyApplication(){}
          80 
          81   Inner classes:
          82     [inner class info: #1 levin/test/anonymous/MyApplication$1, outer class info: #0
          83      inner name: #0, accessflags: 0 default]
          84   Enclosing Method: #66  #77 levin/test/anonymous/MyApplication.print(Ljava/lang/Integer;)V
          85 }

           1 // Compiled from Printer.java (version 1.6 : 50.0, super bit)
           2 class levin.test.anonymous.MyApplication {
           3   
           4   // Field descriptor #6 I
           5   private int field;
           6   
           7   // Method descriptor #8 ()V
           8   // Stack: 2, Locals: 1
           9   MyApplication();
          10      0  aload_0 [this]
          11      1  invokespecial java.lang.Object() [10]
          12      4  aload_0 [this]
          13      5  bipush 10
          14      7  putfield levin.test.anonymous.MyApplication.field : int [12]
          15     10  return
          16       Line numbers:
          17         [pc: 0, line: 7]
          18         [pc: 4, line: 8]
          19         [pc: 10, line: 7]
          20       Local variable table:
          21         [pc: 0, pc: 11] local: this index: 0 type: levin.test.anonymous.MyApplication
          22   
          23   // Method descriptor #19 (Ljava/lang/Integer;)V
          24   // Stack: 6, Locals: 7
          25   public void print(java.lang.Integer param);
          26      0  ldc2_w <Long 100> [20]
          27      3  lstore_2 [local]
          28      4  aload_1 [param]
          29      5  invokevirtual java.lang.Integer.longValue() : long [22]
          30      8  ldc2_w <Long 100> [20]
          31     11  ladd
          32     12  lstore 4 [local2]
          33     14  new levin.test.anonymous.MyApplication$1 [28]
          34     17  dup
          35     18  aload_0 [this]
          36     19  lload 4 [local2]
          37     21  aload_1 [param]
          38     22  invokespecial levin.test.anonymous.MyApplication$1(levin.test.anonymous.MyApplication, long, java.lang.Integer) [30]
          39     25  astore 6 [printer]
          40     27  aload 6 [printer]
          41     29  invokeinterface levin.test.anonymous.Printer.print() : void [33] [nargs: 1]
          42     34  return
          43       Line numbers:
          44         [pc: 0, line: 11]
          45         [pc: 4, line: 12]
          46         [pc: 14, line: 13]
          47         [pc: 27, line: 22]
          48         [pc: 34, line: 23]
          49       Local variable table:
          50         [pc: 0, pc: 35] local: this index: 0 type: levin.test.anonymous.MyApplication
          51         [pc: 0, pc: 35] local: param index: 1 type: java.lang.Integer
          52         [pc: 4, pc: 35] local: local index: 2 type: long
          53         [pc: 14, pc: 35] local: local2 index: 4 type: long
          54         [pc: 27, pc: 35] local: printer index: 6 type: levin.test.anonymous.Printer
          55   
          56   // Method descriptor #45 (Llevin/test/anonymous/MyApplication;)I
          57   // Stack: 1, Locals: 1
          58   static synthetic int access$0(levin.test.anonymous.MyApplication arg0);
          59     0  aload_0 [arg0]
          60     1  getfield levin.test.anonymous.MyApplication.field : int [12]
          61     4  ireturn
          62       Line numbers:
          63         [pc: 0, line: 8]
          64 
          65   Inner classes:
          66     [inner class info: #28 levin/test/anonymous/MyApplication$1, outer class info: #0
          67      inner name: #0, accessflags: 0 default]
          68 }

          從這兩段字節(jié)碼中可以看出,編譯器為我們的匿名類起了一個叫MyApplication$1的名字,它包含了三個final字段(這里synthetic修飾符是指這些字段是由編譯器生成的,它們并不存在于源代碼中):

          MyApplication的應(yīng)用this$0

          longval$local2

          Integer引用val$param

          這些字段在構(gòu)造函數(shù)中賦值,而構(gòu)造函數(shù)則是在MyApplication.print()方法中調(diào)用。

          由此,我們可以得出一個結(jié)論:Java對匿名內(nèi)部類的實現(xiàn)是通過編譯器來支持的,即通過編譯器幫我們產(chǎn)生一個匿名類的類名,將所有在匿名類中用到的局部變量和參數(shù)做為內(nèi)部類的final字段,同是內(nèi)部類還會引用外部類的實例。其實這里少了local的變量,這是因為local是編譯器常量,編譯器對它做了替換的優(yōu)化。

          其實Java中很多語法都是通過編譯器來支持的,而在虛擬機(jī)/字節(jié)碼上并沒有什么區(qū)別,比如這里的final關(guān)鍵字,其實細(xì)心的人會發(fā)現(xiàn)在字節(jié)碼中,param參數(shù)并沒有final修飾,而final本身的很多實現(xiàn)就是由編譯器支持的。類似的還有Java中得泛型和逆變、協(xié)變等。這是題外話。

           

          有了這個基礎(chǔ)后,我們就可以來分析為什么有些要用final修飾,有些卻不用的問題。

          首先我們來分析local2變量,在匿名類中,它是通過構(gòu)造函數(shù)傳入到匿名類字段中的,因為它是基本類型,因而在夠著函數(shù)中賦值時(撇開對函數(shù)參數(shù)傳遞不同虛擬機(jī)的不同實現(xiàn)而產(chǎn)生的不同效果),它事實上只是值的拷貝;因而加入我們可以在匿名類中得print()方法中對它賦值,那么這個賦值對外部類中得local2變量不會有影響,而程序員在讀代碼中,是從上往下讀的,所以很容易誤認(rèn)為這段代碼賦值會對外部類中得local2變量本身產(chǎn)生影響,何況在源碼中他們的名字都是一樣的,所以我認(rèn)為了避免這種confuse導(dǎo)致的一些問題,Java設(shè)計者才設(shè)計出了這樣的語法。

          對引用類型,其實也是一樣的,因為引用的傳遞事實上也只是傳遞引用的數(shù)值(簡單的可以理解成為地址),因而對param,如果可以在匿名類中賦值,也不會在外部類的print()后續(xù)方法產(chǎn)生影響。雖然這樣,我們還是可以在內(nèi)部類中改變引用內(nèi)部的值的,如果引用類型不是只讀類型的話;在這里Integer是只讀類型,因而我們沒法這樣做。(如果學(xué)過C++的童鞋可以想想常量指針和指針常量的區(qū)別)。

           

          現(xiàn)在還剩下最后一個問題:為什么引用外部類的字段卻是可以不用final修飾的呢?細(xì)心的童鞋可能也已經(jīng)發(fā)現(xiàn)答案了,因為內(nèi)部類保存了外部類的引用,因而內(nèi)部類中對任何字段的修改都回真實的反應(yīng)到外部類實例本身上,所以不需要用final來修飾它。

           

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

          加點題外話吧。

          首先是,對這里的字節(jié)碼,其實還有一點可以借鑒的地方,就是內(nèi)部類在使用外部類的字段時不是直接取值,而是通過編譯器在外部類中生成的靜態(tài)的access$0()方法來取值,我的理解,這里Java設(shè)計者想盡量避免其他類直接訪問一個類的數(shù)據(jù)成員,同時生成的access$0()方法還可以被其他類所使用,這遵循了面向?qū)ο笤O(shè)計中的兩個重要原則:封裝和復(fù)用。

           

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

           

          之前有進(jìn)過某著名高校的研究生群,即使在那里,碼農(nóng)論也是甚囂塵上,其實碼農(nóng)不碼農(nóng)并不是因為程序員這個職位引起的,而是個人引起的,我們要不斷理解代碼內(nèi)部的本質(zhì)才能避免一直做碼農(nóng)的命運(yùn)那。個人愚見而已,呵呵。



           

           

          posted on 2011-11-23 01:49 DLevin 閱讀(9523) 評論(8)  編輯  收藏 所屬分類: Core Java

          FeedBack:
          # re: [多問幾個為什么]為什么匿名內(nèi)部類中引用的局部變量和參數(shù)需要final而成員字段不用?[未登錄]
          2011-11-23 10:08 | tester
          主要就是一句話: 字段和局部變量的生命周期不一樣。
          樓主的帖子我沒太看明白,但下面這個我看明白了。
          請參考:
          http://stackoverflow.com/questions/5801829/why-a-non-final-local-variable-cannot-be-used-inside-an-inner-class-and-inste  回復(fù)  更多評論
            
          # re: [多問幾個為什么]為什么匿名內(nèi)部類中引用的局部變量和參數(shù)需要final而成員字段不用?
          2011-11-23 10:30 | 何楊
          碼農(nóng)都想擺脫,我身邊的人都在學(xué)習(xí)外語好當(dāng)PM以擺脫碼農(nóng)。  回復(fù)  更多評論
            
          # re: [多問幾個為什么]為什么匿名內(nèi)部類中引用的局部變量和參數(shù)需要final而成員字段不用?
          2011-11-23 11:27 | 瘋狂
          分析的不錯,但有些地方不是很明確,實際上是為了保持內(nèi)外一致。設(shè)置成final是為了防止外部重新賦值。也就是和copy有關(guān)系,既然是copy,就要內(nèi)外一致。針對外部類的字段卻是可以不用final修飾,這個因為內(nèi)部類最終只有this引用,其實和傳遞引用局部變量是一個機(jī)制。  回復(fù)  更多評論
            
          # re: [多問幾個為什么]為什么匿名內(nèi)部類中引用的局部變量和參數(shù)需要final而成員字段不用?
          2011-11-23 13:07 | DLevin
          我感覺應(yīng)該不是生命周期引起的,Java里的生命周期是由虛擬機(jī)管理的,所以局部變量和外部類實例難說那個生命周期更長,在你給的鏈接中,這段話到是一個蠻好的解釋:So Java could copy the value of the variable w/o bothering with this compiler error, but it instead forces you to declare the variable as final to tell you "hey, remember it gets copied into the inner class, so if you could change it afterwards, then you get a severe inconsistency. So you can't change it, and we're clear with that." @tester
            回復(fù)  更多評論
            
          # re: [多問幾個為什么]為什么匿名內(nèi)部類中引用的局部變量和參數(shù)需要final而成員字段不用?
          2011-11-23 13:08 | DLevin
          恩,我貌似表述的有點繁瑣了,你這個解釋更加簡潔明了一些~~~~@瘋狂
            回復(fù)  更多評論
            
          # re: [多問幾個為什么]為什么匿名內(nèi)部類中引用的局部變量和參數(shù)需要final而成員字段不用?[未登錄]
          2012-05-15 17:03 | 張君
          差不多勒 我再消化消化   回復(fù)  更多評論
            
          # re: [多問幾個為什么]為什么匿名內(nèi)部類中引用的局部變量和參數(shù)需要final而成員字段不用?
          2014-12-11 15:17 | g897
          為什么引用外部類的字段卻是可以不用final修飾的呢?細(xì)心的童鞋可能也已經(jīng)發(fā)現(xiàn)答案了,因為內(nèi)部類保存了外部類的引用,因而內(nèi)部類中對任何字段的修改都回真實的反應(yīng)到外部類實例本身上,所以不需要用final來修飾它。
          ///////////////////////////////////////////////////////////////////////
          難道不是因為全局變量不會被自動回收,所以不會出現(xiàn)局部內(nèi)部類調(diào)用該成員變量時該變量已經(jīng)被自動回收的情況,才不用定義final嗎?

          新手,請指教  回復(fù)  更多評論
            
          # re: [多問幾個為什么]為什么匿名內(nèi)部類中引用的局部變量和參數(shù)需要final而成員字段不用?
          2014-12-12 19:08 | DLevin
          @g897
          final能控制回收流程?你記錯成finalize了?Java哪來的全局變量,又哪來的不會自動回收的說法?  回復(fù)  更多評論
            
          主站蜘蛛池模板: 安塞县| 青冈县| 文山县| 夏邑县| 满洲里市| 张掖市| 璧山县| 五莲县| 衢州市| 平邑县| 五家渠市| 丰原市| 育儿| 南城县| 昆山市| 尤溪县| 东平县| 长兴县| 西丰县| 曲阳县| 蓝田县| 子洲县| 博白县| 茂名市| 沾益县| 保康县| 岢岚县| 开鲁县| 安塞县| 滁州市| 墨竹工卡县| 拉萨市| 白河县| 赣榆县| 广水市| 绥德县| 霍城县| 金华市| 西华县| 彝良县| 额敏县|