1. 程式人生 > >關於為什麼jdk 8以前匿名內部類引數必須為final型別的問題

關於為什麼jdk 8以前匿名內部類引數必須為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,但我們無法改變這個區域性變數的引用值,如果改變就會編譯報錯。