[多問幾個為什么]為什么匿名內部類中引用的局部變量和參數需要final而成員字段不用?(轉)


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

假如我們有以下的代碼:

 

 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要在匿名內部類的print()方法中使用,因而它要用final修飾;local/local2是局部變量,因而也需要final修飾;而field是外部類MyApplication的字段,因而不需要final修飾。這種設計是基於什么理由呢?

 

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

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

根據這兩點信息我們就可以分析,可能這些變量會在匿名內部類的字段中保存着,並且在構造的時候將他們的值/引用傳入內部類。這樣就可以保證同時實現上述兩點了。

 

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

 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  }

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

MyApplication的應用this$0

longval$local2

Integer引用val$param

這些字段在構造函數中賦值,而構造函數則是在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這樣設計也是合理的。是啊,語法其實就一幫人創建的一種解決某些問題的方案,當然有合理和不合理之分,我們其實不用對它視若神聖。

 

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


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com