細說匿名內部類引用方法局部變量時為什么需要聲明為final


一、前言

  在研究公司某個項目的源碼,看到前人使用了挺多內部類,內部類平時我用的比較多的是匿名內部類,平時用的多的是匿名內部類,其他形式的用的比較少,然后我就有個疑惑:到底內部類是基於什么樣的考慮,才讓java設計者搞這么一套實現?還有,平時在什么情境下會考慮使用內部類呢?這個我將在另外一篇博文進行介紹,詳情參見:xxxxxx,本篇博文是在查閱內部類相關資料時,看到很多博文在解釋為什么匿名內部類在引用局部變量時需要將局部變量指定為final(不可變)說的不清楚,所以這邊我將我的理解寫出來,以供參考~

二、正文

  本篇博文基於的Java環境是:jdk1.6,在jdk1.8中,匿名內部類引用局部變量時,已經廢棄使用final修飾局部變量,但這並不意味着jdk1.8在實現方面有什么很大的變動,這個博文最后也會給予說明。

  在討論本篇博文之前,需要與看官達成一些共識:

  1)Java中數據類型分為兩大類:基本數據類型和引用數據類型。相應的,變量也分為基本類型和引用類型。基本類型的變量保存原始值,即它代表的值就是數值本身,而引用類型的變量保存引用值,"引用值"指向內存空間的地址(也就是地址值),代表了某個對象的引用,而不是對象本身,對象本身存放在這個引用值所表示的地址的位置

  2)final修飾的變量,賦予過初值之后,不允許變更。對基本類型而言,其保存的原始值不能變更,對引用類型而言,其引用的地址值不允許變更,也就是說不能引用其他對象

    final int a = 1;
    a = 2;//編譯報錯
        
    final Integer b = new Integer(1);
    b = new Integer(2);//編譯報錯

  先來個匿名內部類使用局部變量的例子:

public class OutClass {
    private String msg = "OutClass's msg";
    
    public void check(final String m){
        (new Thread(){
            public void run(){
                System.out.println(m);
                System.out.println(msg);
            }
        }).start();
    }
    public static void main(String[]args){
        OutClass oc = new OutClass();
        oc.check("Need to pass to inner Class");
    }
    
    
}
//輸出
Need to pass to inner Class
OutClass's msg

  編譯器編譯的時候,匿名內部類會生成一個:外部類$x.class,其中x是正整數,這個實例中匿名內部類為:OutClass$1.class,我們可以使用jdk自帶的反編譯工具,將生成的這個class反編譯出來看下,在dos窗口執行:javap -c OutClass$1.class,

  我相信,只要熟悉內部類的童鞋,都知道,如果要實例化內部類,那么必須先實例化外部類,這就是原因所在,內部類需要持有外部類的一個引用,這也是內部類能無條件訪問外部類所有成員方法和成員變量的根本原因。

   通過上面反編譯出來的內容可以看出,如果匿名內部類使用了局部變量,那么編譯器會將使用的值拷貝一份,作為構造函數的一個參數傳遞進來(構造函數是編譯器自動添加)。因為局部變量在方法或者代碼塊執行完畢,就會被銷毀,所以編譯器在編譯的時候,就拷貝了一份局部變量存儲的字面值或者地址值,這樣局部變量被銷毀時,匿名內部類依然擁有之前傳遞進來的值。現在我們從語義上來理解下Java設計者的考慮:假如傳遞到匿名內部類的局部變量,不加final修飾,那么意味着局部變量可以改變,這就意味着匿名內部類里面值的變更和外部的變量的變更不能同步,雖然內部類持有的是局部變量值的拷貝,但是語義應該保持一致,語義保持一致的前提是值要能同步,因為java編譯器的設計無法提供兩邊同步變更的機制,所以直接鎖死,不允許內外變更~

 1 package com.finalparams.main;
 2 
 3 public class Main {
 4     public static void main(String[]args){
 5         Main m = new Main();
 6         m.show();
 7     }
 8     
 9     public void show(){
10         int a = 1;
11         (new innerClassInterface(){
12             public void doChange(){
13                 a = 3;//編譯報錯:Cannot refer to the non-final local variable a defined in an enclosing scope
14             }
15         }).doChange();
16         System.out.println(a);
17     }
18 }
19 
20 interface innerClassInterface{
21     public void doChange();
22 }

  以上代碼第13行編譯報錯,假設編譯器允許編譯過去,按照編譯器現有的實現,內部類里面的變更是沒有辦法反應到外面的, 也就是里面變更了,但是外部還是沒變,這樣是不能接受的,所以直接鎖死,不讓變更~

 

  特別說明:在jdk1.8之后,匿名內部類使用局部變量的時候,局部變量已經不需要使用final修飾了,可以編譯通過,那是不是說編譯器的實現機制變了呢?非也,只是編譯器在編譯的時候不允許對這個變量進行變更,也就是說,如果你內外只是使用這個變量,而不進行重新賦值,那么就編譯通過,如果內外有重新賦值,那么還是會報編譯錯誤:

......................................................................................
public void show(){
        int a = 1;
        (new innerClassInterface(){
            public void doChange(){
                System.out.println(a);//jdk1.8之前編譯報錯,1.8之后,不報錯,因為沒有改變變量的值
            }
        }).doChange();
        System.out.println(a);
    }
......................................................................................
public void show(){
        int a = 1;
        (new innerClassInterface(){
            public void doChange(){
                System.out.println(a);//jdk1.8之前編譯報錯,1.8之后,不報錯,因為沒有改變變量的值
                a = 4;//jdk1.8編譯報錯
            }
        }).doChange();
        System.out.println(a);
    }

 

三、參考鏈接

       https://www.zhihu.com/question/21395848

  https://zhidao.baidu.com/question/266984801594415645.html

  

四、聯系本人

  為方便沒有博客園賬號的讀者交流,特意建立一個企鵝群(純公益,非利益相關),讀者如果有對博文不明之處,歡迎加群交流:261746360,小杜比亞-博客園


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM