[多问几个为什么]为什么匿名内部类中引用的局部变量和参数需要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这样设计也是合理的。是啊,语法其实就一帮人创建的一种解决某些问题的方案,当然有合理和不合理之分,我们其实不用对它视若神圣。

 

之前有进过某著名高校的研究生群,即使在那里,码农论也是甚嚣尘上,其实码农不码农并不是因为程序员这个职位引起的,而是个人引起的,我们要不断理解代码内部的本质才能避免一直做码农的命运那。个人愚见而已,呵呵。


相关文章
|
4月前
|
存储 Java
成员变量与实例变量的区别与用途详解
成员变量与实例变量的区别与用途详解
|
6月前
|
C语言
C11中类成员变量定义时初始化问题
C11中类成员变量定义时初始化问题
69 0
|
存储 Java
浅谈匿名内部类和局部内部类只能访问final变量
浅谈匿名内部类和局部内部类只能访问final变量
81 0
|
存储 算法 C语言
27 C++ - 对象成员变量和函数的存储
27 C++ - 对象成员变量和函数的存储
43 0
关于static修饰的成员方法成员成员变量的相关讨论
关于static修饰的成员方法成员成员变量的相关讨论
方法引用符、引用类方法、引用对象的实例方法、引用类的实例方法及引用构造器
方法引用符、引用类方法、引用对象的实例方法、引用类的实例方法及引用构造器
91 0
定义了一个类A,S是类外的一个函数,通过A.S=S进行赋值
假设类 A 已经定义好了,现在可以通过 A.S = S 的方式将函数 S 赋值给类 A。这样做的效果是,将 S 函数作为类 A 的一个属性,并且可以通过该属性来调用函数 S。 下面是一个简单的例子:
使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变
使用final关键字修饰一个变量时,是指引用变量不能变,但是引用变量所指向的对象中的内容还是可以改变的。
409 0
使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变
|
JavaScript 安全 前端开发
你知道匿名内部类、Lambda表达式为嘛只能使用外部final的变量吗?
你知道匿名内部类、Lambda表达式为嘛只能使用外部final的变量吗?
你知道匿名内部类、Lambda表达式为嘛只能使用外部final的变量吗?