為什么Java中lambda表達式不能改變外部變量的值,也不能定義自己的同名的本地變量呢?


作者:blindpirate
鏈接:https://www.zhihu.com/question/361639494/answer/948286842
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

TL;DR的回答如下:

JLS 15.27.2 提到:

The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.

在Java的線程模型中,棧幀中的局部變量是線程私有的,永遠不需要進行同步。假如說允許通過匿名內部類把棧幀中的變量地址泄漏出去(逃逸),就會引發非常可怕的后果:一份“本來被Java線程模型規定永遠是線程私有的數據”可能被並發訪問!哪怕它不被並發訪問,棧中變量的內存地址泄漏到棧幀之外這件事本身已經足夠危險了,這是Java這種內存安全的語言絕對無法容忍的(來自評論區

補充)。

 

這才是本質原因。


下面是比較長的答案。對於如下代碼:

public void doSomething() {
    int value = 0;
    IntStream.range(0, 10).forEach(i -> value++ );
}

你會得到一個編譯錯誤:Variable used in lambda expression should be final or effectively final。如果從頭分析一下跟這個問題相關的知識,事情要從Java 8之前就存在的匿名內部類說起:

public void doSomething() {
    int value = 0;
    Executors.newSingleThreadExecutor().submit(new Runnable() {
        @Override
	public void run() {
	    value++;
	}
    });
}

同樣,你會得到一個編譯錯誤:Variable is accessed within inner class. Needs to be declared final.

第一個問題,為什么存在這樣的限制?

要回答這個問題,我們需要首先明白,匿名內部類外面的value和里面的value是同一個內存地址中的數據么?

很明顯不是,因為我們都知道,局部變量存在於棧幀的局部變量表中,一旦方法結束,棧幀被銷毀,這個變量(這份數據)就不再存在,但是匿名內部類中的value可能在棧幀銷毀后繼續存在(比如在這個例子中,匿名內部類被提交到了線程池中)。

所以,只有一個可能,在匿名內部類被創建的時候,被捕獲的局部變量發生了復制。如果我們允許在匿名內部類中執行value++操作,帶來的后果就是,匿名內部類中的value的拷貝被更新了,但是原先的value不會受到任何影響(因為它可能已經不存在了)——你看上去好像兩個value是同一個地址,同一份數據,但是實際上發生了拷貝,和方法調用的值傳遞如出一轍。這是很可怕的一件事情,它會讓你誤以為,在匿名內部類中執行value++會改變原先的局部變量value。

這還不是最可怕的。最可怕的是,如果允許匿名內部類修改外面的局部變量,會顛覆掉整個Java線程模型!!!!!!

JLS 15.27.2 提到:

The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.

在Java的線程模型中,棧幀中的局部變量是線程私有的,永遠不需要進行同步。但是,假如說我們通過匿名內部類把棧幀中的變量地址泄漏出去,就會引發非常可怕的后果:一份“本來被Java線程模型規定永遠是線程私有的數據”可能被並發訪問!!!

因此,在Java 8之前,編譯器會強迫你加上一個final關鍵字:

public void doSomething() {
    final int value = 0;  // 不聲明final不給過編譯,你給老子死了這條修改的心吧
    Executors.newSingleThreadExecutor().submit(new Runnable() {
        @Override
	public void run() {
	    System.out.println(value);
	}
    });
}

第二個問題:那為什么Java 8之后我可以不寫final了呢?

Java 8引入了lambda表達式,我們從此可以非常方便地編寫大量的小代碼塊,但是在捕獲外圍的局部變量這件事上,lambda表達式和匿名內部類沒有任何區別——被捕獲的局部變量必須是final的。這就帶來了一個問題,繼續堅持把局部變量聲明成final的話,煩也煩死了。 因此,JLS做出了一個妥協:

假如一個局部變量在整個生命周期中都沒有被改變(指向),那么它就是effectively final的——換句話說,不是final,勝似final。這樣的局部變量也允許被lambda表達式或者匿名內部類所捕獲,不過只能看不能摸——可以讀取,但是不能修改。

下一個問題是,老子就是想在lambda表達式里面改外面的值!你咬我啊!

IDEA早已看穿了一切:

還記得我在之前的文章中強調的么?任何錯誤,你都可以按萬能鍵Alt+Enter:

blindpirate:「每日一題」走上人生巔峰的快捷鍵​zhuanlan.zhihu.com圖標

為什么轉換成一個AtomicInteger就可以了呢?這跟線程安全沒有半毛錢關系,純粹是利用了這樣一個技巧:AtomicInteger可以當作int的容器。因為它是在堆上被分配的,我們完全沒有改變這個局部變量的指向(effectively final成立),就達到了修改其中數據的目的。


免責聲明!

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



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