前不久在學習中意外發現了自己原來忽略的一個小知識點,挺有意思的,現在我來給大家分享一下!
我們先來看一段代碼
public class Hello {
public static void main(String[] args) {
String str = "haha";
new Thread() {
@Override
public void run() {
System.out.println(str);
}
}.start();
}
}
現在我問問大家,這個打印的程序的結果是什麼?
可能大部分人毫不猶豫的會說:打印“haha”。其實這個程序根本就編譯不通過(有點答非所問的感覺,哈哈)。
因爲**在JDK8之前,如果我們在匿名內部類中需要訪問局部變量,那麼這個局部變量必須用final修飾符修飾。**這裏所說的匿名內部類指的是在外部類的成員方法中定義的內部類。既然是在方法中創建的內部類,必然會在某些業務邏輯中出現訪問這個方法的局部變量的需求。那麼我們下面就會研究這種情況。
爲什麼java語法要求我們需要用final修飾呢?想了想沒有什麼答案,那我們就通過jd-gui反編譯工具一探究竟,我們對匿名內部類的字節碼文件進行反編譯得到以下內容。
我們可以看到匿名內部類的構造器中傳入了一個參數,我們可以推理出這個參數就是底層傳入的str的值,但因爲反編譯工具的某種疏忽將構造器的方法體寫成了空,事實上真正的反編譯代碼應該是下面:
public class Hello$1 extends Thread {
private String val$str;
Hello$1(String paramString) {
this.val$str = paramString;
}
public void run() {
System.out.println(this.val$str);
}
}
也就是說匿名內部類之所以可以訪問局部變量,是因爲在底層將這個局部變量的值傳入到了匿名內部類中,並且以匿名內部類的成員變量的形式存在,這個值的傳遞過程是通過匿名內部類的構造器完成的。
那麼問題又來了,爲什麼需要用final修飾局部變量呢?
按照習慣,我依舊先給出問題的答案:用final修飾實際上就是爲了保護數據的一致性。
這裏所說的數據一致性,對引用變量來說是引用地址的一致性,對基本類型來說就是值的一致性。
這裏我插一點,final修飾符對變量來說,深層次的理解就是保障變量值的一致性。爲什麼這麼說呢?因爲引用類型變量其本質是存入的是一個引用地址,說白了還是一個值(可以理解爲內存中的地址值)。用final修飾後,這個這個引用變量的地址值不能改變,所以這個引用變量就無法再指向其它對象了。
回到正題,爲什麼需要用final保護數據的一致性呢?
因爲將數據拷貝完成後,如果不用final修飾,則原先的局部變量可以發生變化。這裏到了問題的核心了,如果局部變量發生變化後,匿名內部類是不知道的(因爲他只是拷貝了局不變量的值,並不是直接使用的局部變量)。這裏舉個栗子:原先局部變量指向的是對象A,在創建匿名內部類後,匿名內部類中的成員變量也指向A對象。但過了一段時間局部變量的值指向另外一個B對象,但此時匿名內部類中還是指向原先的A對象。那麼程序再接着運行下去,可能就會導致程序運行的結果與預期不同。
介紹到這裏,關於爲什麼匿名內部類訪問局部變量需要加final修飾符的原理基本講完了。那現在我們來談一談JDK8對這一問題的新的知識點。**在JDK8中如果我們在匿名內部類中需要訪問局部變量,那麼這個局部變量不需要用final修飾符修飾。**看似是一種編譯機制的改變,實際上就是一個語法糖(底層還是幫你加了final)。但通過反編譯沒有看到底層爲我們加上final,但我們無法改變這個局部變量的引用值,如果改變就會編譯報錯。