這里因為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這樣設計也是合理的。是啊,語法其實就一幫人創建的一種解決某些問題的方案,當然有合理和不合理之分,我們其實不用對它視若神聖。
之前有進過某著名高校的研究生群,即使在那里,碼農論也是甚囂塵上,其實碼農不碼農並不是因為程序員這個職位引起的,而是個人引起的,我們要不斷理解代碼內部的本質才能避免一直做碼農的命運那。個人愚見而已,呵呵。