我們在一個方法中定義匿名内部類通路方法的本地變量時常常會發現編譯時出錯,被告知“需要被聲明為最終類型",甚是疑惑,于是在網上搜尋其原因, 在此作一總結。
局部内部類(在方法内部定義的類)中無法直接通路方法中的局部變量,須修飾其為final
1:在方法内聲明的本地變量的生命周期與局部内部類對象的生命周期不一緻進而導緻了這個問題。前者在一個方法運作結束後就随之被銷毀。而後者生命周期的終點卻并不在此處,隻有當該對象不再被引用時,它才會被GC回收。倘若匿名内部類可以直接通路不被final修飾的本地變量,那麼就有可能出現一個奇怪的現象:對象在通路一個已經不存在的變量。
2:我們假設局部内部類中可以直接通路方法中的局部變量,且不需要其為final型。我們知道,在内部類中通路變量實際上是在通路該變量的複制品,如果上述條件成立,無論是基本類型還是引用類型,那麼一旦局部變量實體或者複制品任何一方發生改變,都不能互相同步,進而造成變量的實體與複制品不一緻,想象一下你看着是在通路一個變量,然而你得到的值卻與實際的值不同,是以這樣就毫無意義可言。
為了更加清晰深刻的了解Java在處理匿名内部類通路final修飾的本地變量時的處理過程,先看看下面這一段代碼
代碼清單FinalKeywordTest.java
public class FinalKeywordTest {
public static void main(String[] args) {
final int i = 10;
final Person person = new Person("penny", 26);
new Thread() {
@Override
public void run() {
System.out.println(i + 1);
System.out.println(person);
}
};
}
}
class Person{
String name;
int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
首先通過JDK的工具javap檢視一下這個Java檔案被編譯成什麼樣子的
javap -v FinalKeywordTest.class > 1.txt
javap -v FinalKeywordTest$1.class > 2.txt
這裡得到的結果分别列印到1.txt和2.txt中了,由于我們研究的是匿名内部類如何通路final修飾的本地變量,是以我們主要看2.txt即可
Classfile /D:/Workspace/eclipse-for-temp/test/bin/com/kmter/test/FinalKeywordTest$1.class
Last modified 2014-12-21; size 772 bytes
MD5 checksum b2d663f92b7895759e9a26f6124ef489
Compiled from "FinalKeywordTest.java"
class com.kmter.test.FinalKeywordTest$1 extends java.lang.Thread
SourceFile: "FinalKeywordTest.java"
EnclosingMethod: #38.#40 // com.kmter.test.FinalKeywordTest.main
InnerClasses:
#1; //class com/kmter/test/FinalKeywordTest$1
minor version: 0
major version: 51
flags: ACC_SUPER
Constant pool:
#1 = Class #2 // com/kmter/test/FinalKeywordTest$1
#2 = Utf8 com/kmter/test/FinalKeywordTest$1
#3 = Class #4 // java/lang/Thread
#4 = Utf8 java/lang/Thread
#5 = Utf8 val$person
#6 = Utf8 Lcom/kmter/test/Person;
#7 = Utf8 <init>
#8 = Utf8 (Lcom/kmter/test/Person;)V
#9 = Utf8 Code
#10 = Fieldref #1.#11 // com/kmter/test/FinalKeywordTest$1.val$person:Lcom/kmter/test/Person;
//這裡可以看到被final修飾的Person對象在匿名内部類中直接展現為常量池的一個對象引用,這個對象的引用是從FinalKeywordTest類的main方法中複制過來的
#11 = NameAndType #5:#6 // val$person:Lcom/kmter/test/Person;
#12 = Methodref #3.#13 // java/lang/Thread."<init>":()V
#13 = NameAndType #7:#14 // "<init>":()V
#14 = Utf8 ()V
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/kmter/test/FinalKeywordTest$1;
#19 = Utf8 run
#20 = Fieldref #21.#23 // java/lang/System.out:Ljava/io/PrintStream;
#21 = Class #22 // java/lang/System
#22 = Utf8 java/lang/System
#23 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Methodref #27.#29 // java/io/PrintStream.println:(I)V
#27 = Class #28 // java/io/PrintStream
#28 = Utf8 java/io/PrintStream
#29 = NameAndType #30:#31 // println:(I)V
#30 = Utf8 println
#31 = Utf8 (I)V
#32 = Methodref #27.#33 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#33 = NameAndType #30:#34 // println:(Ljava/lang/Object;)V
#34 = Utf8 (Ljava/lang/Object;)V
#35 = Utf8 SourceFile
#36 = Utf8 FinalKeywordTest.java
#37 = Utf8 EnclosingMethod
#38 = Class #39 // com/kmter/test/FinalKeywordTest
#39 = Utf8 com/kmter/test/FinalKeywordTest
#40 = NameAndType #41:#42 // main:([Ljava/lang/String;)V
#41 = Utf8 main
#42 = Utf8 ([Ljava/lang/String;)V
#43 = Utf8 InnerClasses
{
com.kmter.test.FinalKeywordTest$1(com.kmter.test.Person);
flags:
//預設構造函數位元組碼
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #10 // Field val$person:Lcom/kmter/test/Person;
//将執行個體域person指派為常量池中的#10(這裡是關鍵,将main方法中的person對象在本類中認為是一個執行個體變量)
5: aload_0
6: invokespecial #12 // Method java/lang/Thread."<init>":()V
9: return
LineNumberTable:
line 1: 0
line 7: 5
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/kmter/test/FinalKeywordTest$1;
//run方法的位元組碼
public void run();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
3: bipush 11 //将單位元組常量11壓入棧頂
5: invokevirtual #26 // Method java/io/PrintStream.println:(I)V
//執行println(int)方法
8: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_0 //将this指針壓入棧頂
12: getfield #10 // Field val$person:Lcom/kmter/test/Person;
//拿到person對象并将其壓入棧頂
15: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
//執行println(Object)方法
18: return
LineNumberTable:
line 11: 0
line 12: 8
line 13: 18
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 this Lcom/kmter/test/FinalKeywordTest$1;
}
通過上面的位元組碼分析我們可以發現匿名内部類通路本地變量的時候是直接将本地對象的引用當作了該類中的一個執行個體變量來處理的,因為是由final修飾,是以不用擔心指針發生更改