為什么Java匿名內部類訪問的方法參數或方法局部變量需要被final修飾


分析

1、內部類(不論是否是匿名內部類)可訪問外部類的變量(包括外部類的類變量、實例變量、外部類方法的局部變量等)、方法:可修改變量值、調用方法等。內部類定義時的位置有兩種:

在外部類的方法內:此時該內部類只能是匿名內部類(語法上不支持在方法內定義非匿名類)。此時內部類可訪問上述所有變量。

不在外部類的方法內:此時該內部類可以是匿名內部類也可以不是匿名內部類。此時內部類無法訪問外部類各方法的局部變量。

2、若在外部類的方法內定義類(只能是匿名內部類),則該內部類的實例的生命周期有可能超過局部變量的生命周期(此場景即所謂的閉包,在javascript等很多語言中都有)。典型的是回調函數的場景,示例:

private Animator createAnimatorView(final View view, final int position) {
    MyAnimator animator = new MyAnimator();
    animator.addListener(new AnimatorListener() {
        @Override
        public void onAnimationEnd(Animator arg0) {
            Log.d(TAG, "position=" + position); 
        }
    });
    return animator;
}//onAnimationEnd事件可能在createAnimatorView方法結束后很久才觸發,觸發時用到了方法中的局部變量position

方法執行完后局部變量銷毀了,但內部類可能仍要訪問該局部變量,這時就會出錯,怎么辦?

Java解決方法是將局部變量復制一份到內部類,這樣方法執行完后匿名內部類里仍可使用該變量。但這種實現方式還需要確保在程序員看來他們是同一個,即值始終一樣,怎么做到?

法1:同步。當匿名內部類內對復制值做修改時同步回局部變量、在方法內的匿名內部類之后修改局部變量時復制值也跟着改,這種實現上困難且麻煩。

法2:不用同步,直接將局部變量聲明為final的以使其不可變。Java就是用此法。

結論

1、要求用final的場景:只有 被方法內的匿名內部類訪問的方法內的局部變量(方法參數、方法內的變量)才需要加final。非匿名內部類、方法外的匿名內部類訪問變量時沒有該要求。

局部變量不一定須加final,只有 是局部變量、被匿名內部類訪問到 的變量才必須加final

匿名內部類訪問到的變量不一定須加final,只有訪問的變量是局部變量才必須加final

2、要求用final的原因:匿名內部類在方法內時,匿名內部類對象生命周期可能超過方法內的局部變量的生命周期;為了延續生命周期Java復制了局部變量到匿名內部類,之后需要保證復制值與原始值始終一致;保證一致的方式是將局部變量聲明為final使其不可變。

 

其他

Java 8開始,如果局部變量聲明並初始化后沒有被修改過,則此時該變量也會被當成是final的(稱為effictively final),故此時也可被匿名內部類(或Lambda表達式)訪問。

Informally, a local variable is effectively final if its initial value is never changed -- in other words, declaring it final would not cause a compilation failure. 

 示例:

//正確
Callable<String> helloCallable(String name) {
  String hello = "Hello";
  return () -> (hello + ", " + name);
}

//錯誤
int sum = 0;
list.forEach(e -> { sum += e.size(); }); // ERROR

 

 

 

 

 

 

以下為舊摘

=================

 

  大部分時候,類被定義成一個獨立的程序單元。在某些情況下,也會把一個類放在另一個類的內部定義,這個定義在其他類內部的類就被稱為內部類,包含內部類的類也被稱為外部類。

復制代碼
class Outer
{
    private int a;
    public class Inner
    {
        private int a;
        public void method(int a)
        {
            a++;         //局部變量
            this.a++;      //Inner類成員變量
            Outer.this.a++; //Outer類成員變量
        }
    }
}        
復制代碼

  一般做法是在Outer中寫一個返回Inner類對象的方法

public Inner getInner()
{ return new Inner(); }

  在其他類中使用內部類:

Outer outer = new Outer();
Outer.Inner inner = outer.getInner();
//或者Outer.Inner inner = outer.new Inner();

  static內部類的使用:

Outer.Inner inner = new Outer.Inner();

  匿名內部類不能訪問外部類方法中的局部變量(包括方法參數、方法內的變量),除非變量被聲明為final類型

  1. 這里所說的“匿名內部類”主要是指在其外部類的成員方法內定義,同時完成實例化的類,若其訪問該成員方法中的局部變量,局部變量必須要被final修飾。
  2. 原因是編譯程序實現上的困難:內部類對象的生命周期會超過局部變量的生命周期。局部變量的生命周期:當該方法被調用時,該方法中的局部變量在棧中被創建,當方法調用結束時,退棧,這些局部變量全部死亡。而內部類對象生命周期與其它類一樣:自創建一個匿名內部類對象,系統為該對象分配內存,直到沒有引用變量指向分配給該對象的內存,它才會死亡(被JVM垃圾回收)。所以完全可能出現的一種情況是:成員方法已調用結束,局部變量已死亡,但匿名內部類的對象仍然活着。
  3. 如果匿名內部類的對象訪問了同一個方法中的局部變量,就要求只要匿名內部類對象還活着,那么棧中的那些它要所訪問的局部變量就不能“死亡”。
  4. 解決方法:匿名內部類對象可以訪問同一個方法中被定義為final類型的局部變量。定義為final后,編譯程序的實現方法:對於匿名內部類對象要訪問的所有final類型局部變量,都拷貝成為該對象中的一個數據成員。這樣,即使棧中局部變量已死亡,但被定義為final類型的局部變量的值永遠不變,因而匿名內部類對象在局部變量死亡后,照樣可以訪問final類型的局部變量,因為它自己拷貝了一份,且與原局部變量的值始終一致。

  最后,Java 8更加智能:如果局部變量被方法內的匿名內部類訪問,那么該局部變量相當於自動使用了final修飾。此外,Java 8的λ表達式也與此類似只能訪問final外部變量但不要求用final修飾,不過,變量同樣不能被重新賦值。

 

參考資料:

https://www.cnblogs.com/bootdo/p/10844032.html

http://www.cnblogs.com/eniac12/p/5240100.html

https://mp.weixin.qq.com/s/-2dGPhjbY7TKtR3Un31Kig(Java λ表達式)

 


免責聲明!

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



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