1. 程式人生 > 其它 >假期java自學-----類7

假期java自學-----類7

內部類

2.為什麼區域性內部類和匿名內部類只能訪問區域性final變數?

想必這個問題也曾經困擾過很多人,在討論這個問題之前,先看下面這段程式碼:
publicclassTest{
publicstaticvoidmain(String[]args){

}

publicvoidtest(finalintb){
finalinta=10;
newThread(){
publicvoidrun(){
System.out.println(a);
System.out.println(b);
};
}.start();
}
} 
這段程式碼會被編譯成兩個class檔案:Test.class和Test1.class。預設情況下,編譯器會為匿名內部類和區域性內部類起名為Outterx.class(x為正整數)。

根據上圖可知,test方法中的匿名內部類的名字被起為Test$1。
上段程式碼中,如果把變數a和b前面的任一個final去掉,這段程式碼都編譯不過。我們先考慮這樣一個問題:
當test方法執行完畢之後,變數a的生命週期就結束了,而此時Thread物件的生命週期很可能還沒有結束,那麼在Thread的run方法中繼續訪問變數a就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?Java採用了複製的手段來解決這個問題。將這段程式碼的位元組碼反編譯可以得到下面的內容:

我們看到在run方法中有一條指令:
bipush10
這條指令表示將運算元10壓棧,表示使用的是一個本地區域性變數。這個過程是在編譯期間由編譯器預設進行,如果這個變數的值在編譯期間可以確定,則編譯器預設會在匿名內部類(區域性內部類)的常量池中新增一個內容相等的字面量或直接將相應的位元組碼嵌入到執行位元組碼中。這樣一來,匿名內部類使用的變數是另一個區域性變數,只不過值和方法中區域性變數的值相等,因此和方法中的區域性變數完全獨立開。
下面再看一個例子:
publicclassTest{
publicstaticvoidmain(String[]args){

}

publicvoidtest(finalinta){
newThread(){
publicvoidrun(){
System.out.println(a);
};
}.start();
}
}
反編譯得到:

我們看到匿名內部類Test$1的構造器含有兩個引數,一個是指向外部類物件的引用,一個是int型變數,很顯然,這裡是將變數test方法中的形參a以引數的形式傳進來對匿名內部類中的拷貝(變數a的拷貝)進行賦值初始化。
也就說如果區域性變數的值在編譯期間就可以確定,則直接在匿名內部裡面建立一個拷貝。如果區域性變數的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。
從上面可以看出,在run方法中訪問的變數a根本就不是test方法中的區域性變數a。這樣一來就解決了前面所說的生命週期不一致的問題。但是新的問題又來了,既然在run方法中訪問的變數a和test方法中的變數a不是同一個變數,當在run方法中改變變數a的值的話,會出現什麼情況?
對,會造成資料不一致性,這樣就達不到原本的意圖和要求。為了解決這個問題,java編譯器就限定必須將變數a限制為final變數,不允許對變數a進行更改(對於引用型別的變數,是不允許指向新的物件),這樣資料不一致性的問題就得以解決了。
到這裡,想必大家應該清楚為何方法中的區域性變數和形參都必須用final進行限定了。
3.靜態內部類有特殊的地方嗎?
從前面可以知道,靜態內部類是不依賴於外部類的,也就說可以在不建立外部類物件的情況下建立內部類的物件。另外,靜態內部類是不持有指向外部類物件的引用的,這個讀者可以自己嘗試反編譯class檔案看一下就知道了,是沒有Outterthis&0引用的。