Java 內部類與匿名內部類

問:什麼JDK8之前內部類中如果有訪問外部類的成員時,必須要加上final?爲什麼JDK8不用了?

答:我們先不說JDK版本,就說說爲什麼要加final。

我先給出問題的答案:用final修飾實際上就是爲了保護數據的一致性。

這裏所說的數據一致性,對引用變量來說是引用地址的一致性,對基本類型來說就是值的一致性。

這裏我插一點,final修飾符對變量來說,深層次的理解就是保障變量值的一致性。爲什麼這麼說呢?因爲引用類型變量其本質是存入的是一個引用地址,說白了還是一個值(可以理解爲內存中的地址值)。用final修飾後,這個這個引用變量的地址值不能改變,所以這個引用變量就無法再指向其它對象了。

首先,因爲生命週期的原因。方法中的局部變量,方法結束後這個變量就要釋放掉,內部類和外部類其實是處於同一個級別,內部類不會因爲定義在方法中就會隨着方法的執行完畢而跟隨者被銷燬,它的生命週期同外部類相同。問題就來了,那麼當外部類方法執行完畢的時候,這個局部變量肯定也就出棧了,然而內部類的某個方法還沒有執行完,這個時候他所引用的外部變量已經找不到了。

爲了解決這個問題,java會將這個變量複製一份作爲成員變量內置於內部類中,這樣的話,及時外部類方法的局部變量出棧失效,我們也可以引用到複製的內部類中的成員,相當於延長了局部變量的“生命”。 但是這也僅僅解決了局部變量的生命週期與局部內部類的對象的生命週期的不一致性問題。也是爲什麼局部變量要作爲內部類構造方法的參數傳入。

 

回到正題,爲什麼需要用final保護數據的一致性呢?

如果我們不用final修飾外部類方法局部變量,因爲內部類(包括匿名內部類)對於局部變量的引用並不是直接的引用,而是將數據拷貝到自己的類變量中在加以使用,則局部變量可以發生變化。這裏到了問題的核心了,如果局部變量發生變化後,匿名內部類是不知道的(因爲他只是拷貝了局部變量的值,並不是直接使用的局部變量)。這裏舉個栗子:原先局部變量指向的是對象A,在創建匿名內部類後,匿名內部類中的成員變量也指向A對象。但過了一段時間局部變量的值指向另外一個B對象,但此時匿名內部類中還是指向原先的A對象。那麼程序再接着運行下去,可能就會導致程序運行的結果與預期不同。

在JDK8中如果我們在匿名內部類中需要訪問局部變量,那麼這個局部變量不需要用final修飾符修飾。看似是一種編譯機制的改變,實際上就是一個語法糖(底層還是幫你加了final)。但通過反編譯沒有看到底層爲我們加上final,但我們無法改變這個局部變量的引用值,如果改變就會編譯報錯。

匿名內部類反編譯解析:

反編譯下

由於反編譯工具的問題我們無法看到匿名內部類構造器中成員賦值的操作。

從上面三張圖我們可以看到,對於外部類成員變量的引用(Integer  a)將外部類對象淺拷貝到內部類一份,並用final修飾,然後通過此拷貝對象訪問外部類對象成員變量。

對於方法局部變量,我們看到需要使用final修飾(String  s)(JDK 1.8以後不再需要,通過底層語法糖將final添加到局部變量中)並且內部類會通過構造方法將引用的局部變量拷貝到自己的成員變量中(圖2 圖3)

 

局部內部類反編譯解析:

雖然反編譯工具沒有將全部內容展示出來,但是通過標紅地區的代碼,我們還是可以感受出來內部類與匿名內部類都是同樣的處理方式。

 

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

問:剛纔看到內部類,匿名內部類中都隱藏有外部類的強引用,這樣會導致外部類無法進行垃圾回收,我們該如何解決?

答:

1 將內部類定義爲static

2 用static的變量引用匿名內部類的實例或將匿名內部類的實例化操作放到外部類的靜態方法中

3 當使用匿名內部類創建線程是Thread時,如果線程運行沒有完成或者沒有被殺死,它將不會回收,也將導致外部類對象不會被回收,這將導致內存溢出的風險,所以我們要養成爲Thread設置退出邏輯條件的習慣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章