[多问几个为什么]为什么匿名内部类中引用的局部变量和参数需要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

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

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

时间: 2024-10-15 04:28:45

[多问几个为什么]为什么匿名内部类中引用的局部变量和参数需要final而成员字段不用?(转)的相关文章

为什么Java匿名内部类访问的外部局部变量或参数需要被final修饰

大部分时候,类被定义成一个独立的程序单元.在某些情况下,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类,包含内部类的类也被称为外部类. class Outer { private int a; public class Inner { private int a; public void method(int a) { a++; //局部变量 this.a++; //Inner类成员变量 Outer.this.a++; //Outer类成员变量 } } } 一般做法是

JAVA中内部类(匿名内部类)访问的局部变量为什么要用final修饰?

本文主要记录:在JAVA中,(局部)内部类访问某个局部变量,为什么这个局部变量一定需要用final 关键字修饰? 首先,什么是局部变量?这里的局部是:在方法里面定义的变量. 因此,内部类能够访问某局部变量,说明这个内部类不是在类中定义的内部类,而是在方法中定义的内部类,称之为:局部内部类. 局部变量的作用域:局部变量是在某个方法中定义,当该方法执行完成后,局部变量也就消失了.[局部变量分配在JVM的虚拟机栈中,这部分内存空间随着程序的执行自动回收],也即:局部变量的作用域是在 “方法的范围内”.

java 匿名内部类的方法参数需要final吗?

内部类通常都含有回调,引用那个匿名内部类的函数执行完了就没了,所以内部类中引用外面的局部变量需要是final的,这样在回调的时候才能找到那个变量,而如果是外围类的成员变量就不需要是final的,因为内部类本身都会含有一个外围了的引用(外围类.this),所以回调的时候一定可以访问到. 来自知乎http://www.zhihu.com/question/21395848

匿名内部类 调用方法内局部变量

局部匿名类在源代码编译后也是要生成对应的class文件的(一般会是A$1.class这种形式的文件),那么这个二进制文件是独立于其外围类(A.class)的,就是说它无法知道A类中方法的变量.但是A$1.class又确实要访问A类对应方法的局部变量的值...怎么办呢?于是干脆就要求“匿名内部类调用的方法内局部变量必须为final”,这样A$1.class访问A类方法局部变量部分就直接用常量来表示 匿名内部类 调用方法内局部变量,布布扣,bubuko.com

Android onclicklistener中使用外部类变量时为什么需要final修饰【转】

Java内部类详解 说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉.原因是平时编写代码时可能用到的场景不多,用得最多的是在有事件监听的情况下,并且即使用到也很少去总结内部类的用法.今天我们就来一探究竟.下面是本文的目录大纲: 一.内部类基础 二.深入理解内部类 三.内部类的使用场景和好处 四.常见的与内部类相关的笔试面试题 若有不正之处,请多谅解并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/dolphin0520/p/3811

在Java中String类为什么要设计成final?String真的不可变吗?其他基本类型的包装类也是不可变的吗?

最近突然被问到String为什么被设计为不可变,当时有点懵,这个问题一直像bug一样存在,竟然没有发现,没有思考到,在此总结一下. 1.String的不可变String类被final修饰,是不可继承和修改的.当一个String变量被第二次赋值时,不是在原有内存地址上修改数据,而是在内存中重新开辟一块内存地址,并指向新地址. String类为什么要被设计为是final的? 1.不可变性支持线程安全. 2.不可变性支持字符串常量池,提升性能. 3.String字符串作为最常用数据类型之一,不可变防止

.net 工程中引用出现感叹号

在工程中引用出现感叹号,有两个原因 原因1:  这是由于之前引用的Dll文件不见了. 右键有感叹号的项,然后选择 "属性" 里边有一个路径属性 这个路径就是之前这个Dll文件的路径,现在这个文件不在了,你需要找到现在这个文件的路径 右键有感叹号的项,然后选择"移除" 右键"引用",选择添加引用,然后选择那个不在的dll的真实路径 其他的项用相同的方式处理 原因2:可能是引用的.Net版本高于了当前工程的.Net版本 更改所引用的工程文件的.Net

C++中引用的底层实现

为了研究一下C++中引用的底层实现,写了一个小代码验证其中的基本原理. 引用是一个变量的别名,到底会不会为引用申请内存空间?如果申请空间,空间存放的是什么,下面的代码就主要解决这个疑问. 代码如下,详细见代码注释 1 #include <iostream> 2 #include<string> 3 #include <vector> 4 #include <algorithm> 5 using namespace std; 6 7 class Test 8

php 中引用的应用

<?php // http://blog.csdn.net/samxx8/article/details/37564103 /** 在PHP 中引用的意思是:不同的名字访问同一个变量内容. (1) 变量之间的引用: $a = 10 ; $b =$a ; // 此时$b和$a所指向的是同一存储地址 ,如果$a = 11 ; 则zend会开辟一个新的单元从来是的$a = 11 ; $b 还是原来的地址 $a = 10 ; $b =&$a ; // 此时$b 就是$a的别名,比如这个人叫小张,大名