博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[多问几个为什么]为什么匿名内部类中引用的局部变量和参数需要final而成员字段不用?...
阅读量:5880 次
发布时间:2019-06-19

本文共 9157 字,大约阅读时间需要 30 分钟。

昨天有一个比较爱思考的同事和我提起一个问题:为什么匿名内部类使用的局部变量和参数需要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这样设计也是合理的。是啊,语法其实就一帮人创建的一种解决某些问题的方案,当然有合理和不合理之分,我们其实不用对它视若神圣。

 

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

转载地址:http://qbjix.baihongyu.com/

你可能感兴趣的文章
Alpha线性混合实现半透明效果
查看>>
chkconfig 系统服务管理
查看>>
一个简单的运算表达式解释器例子
查看>>
ORACLE---Unit04: SQL(高级查询)
查看>>
Entity Framework Code First 模式-建立多对多联系
查看>>
[LeetCode] Reverse Lists
查看>>
前台页面之<base>标签
查看>>
angular分页插件tm.pagination 解决触发二次请求的问题
查看>>
day08-文件操作
查看>>
教学-45 对象的相等
查看>>
贪食蛇
查看>>
关于Spring 中的事务
查看>>
为什么现在都用面向对象开发,为什么现在都用分层开发结构?
查看>>
【离散数学】 SDUT OJ 偏序关系
查看>>
写给学弟学妹的产品入门建议(持续更新)
查看>>
view视图总结
查看>>
oracle11g 数据库导出报“ EXP-00003:
查看>>
201521123009 《Java程序设计》第11周学习总结
查看>>
可解释的机器学习
查看>>
Python3之多线程学习
查看>>