JDK8之前,局部內部類、匿名內部類訪問的局部變量爲什麼必須要用final修飾

前不久在學習中意外發現了自己原來忽略的一個小知識點,挺有意思的,現在我來給大家分享一下!

我們先來看一段代碼

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,但我們無法改變這個局部變量的引用值,如果改變就會編譯報錯。

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