内部类引用局部变量与外部类成员变量的问题思考

开发 后端
主要针对了匿名内部类使用的局部变量和参数需要final修饰,而外部类的成员变量则不用的问题进行解析。

昨天有一个比较爱思考的同事和我提起一个问题:为什么匿名内部类使用的局部变量和参数需要final修饰,而外部类的成员变量则不用?对这个问题我一直作为默认的语法了,木有仔细想过为什么(在分析完后有点印象在哪本书上看到过,但是就是没有找到,难道是我的幻觉?呵呵)。虽然没有想过,但是还是借着之前研究过字节码的基础上,分析了一些,感觉上是找到了一些答案,分享一下;也希望有大牛给指出一些不足的地方。

假如我们有以下的代码:

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

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

从这两段字节码中可以看出,编译器为我们的匿名类起了一个叫MyApplication$1的名字,它包含了三个final字段(这里synthetic修饰符是指这些字段是由编译器生成的,它们并不存在于源代码中):

MyApplication的应用this$0

long值val$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这样设计也是合理的。是啊,语法其实就一帮人创建的一种解决某些问题的方案,当然有合理和不合理之分,我们其实不用对它视若神圣。

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

原文链接:http://www.blogjava.net/DLevin/archive/2011/11/23/364599.html

【编辑推荐】

  1. Java的ClassLoader机制解析
  2. 11月份的Java面试题新鲜出炉
  3. Java自带的Future多线程模式
  4. 解析Java finally的神秘面纱
  5. 去故就新 Java线程新同步机制
责任编辑:林师授 来源: 上善若水的博客
相关推荐

2020-01-15 11:14:21

Java算法排序

2009-06-11 11:07:25

Java局部内部类Final类型

2020-12-14 10:23:23

Java内部类外部类

2020-11-11 21:26:48

函数变量

2015-01-07 14:41:32

Android全局变量局部变量

2017-02-08 12:28:37

Android变量总结

2023-10-19 13:24:00

Java工具

2009-09-17 13:05:38

Linq局部变量类型

2009-09-22 17:21:24

线程局部变量

2009-08-26 16:37:07

C#迭代器局部变量

2010-03-15 09:32:56

Python函数

2009-09-11 10:07:05

Linq隐式类型化局部

2009-10-12 14:13:00

VB.NET使用局部变

2009-12-15 10:48:54

Ruby局部变量

2018-05-14 09:15:24

Python变量函数

2020-12-03 06:30:11

内部类对象变量

2009-06-11 13:08:29

Java内部类Java编程思想

2009-08-18 17:17:05

C#局部类型

2009-08-27 10:08:36

C#隐含类型局部变量

2021-09-28 07:12:09

函数内存
点赞
收藏

51CTO技术栈公众号