程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 初探Lambda表達式/Java多核編程【4】Lambda變量捕獲

初探Lambda表達式/Java多核編程【4】Lambda變量捕獲

編輯:關於JAVA

初探Lambda表達式/Java多核編程【4】Lambda變量捕獲。本站提示廣大學習愛好者:(初探Lambda表達式/Java多核編程【4】Lambda變量捕獲)文章只能為提供參考,不一定能成為您想要的結果。以下是初探Lambda表達式/Java多核編程【4】Lambda變量捕獲正文


這周開學,上了兩天感覺課好多,學校現在還停水,宿捨網絡也還沒通,簡直爆炸,感覺能靜下心看書的時間越來越少了...寒假還有些看過書之後的存貨,現在寫一點發出來。加上假期兩個月左右都過去了書才看了1/7都不到...還得去續借一下,看來買書多看書少的毛病也得改一改,先致力於把剁手買的書啃完。

另外再次推薦下我現在看的這本書(詳見第0篇),越看越費勁...干貨非常多而且特別干,總之相比於其他書可以說是一頁頂三頁了,每一段都值得仔細琢磨,發現看不懂的還得調轉方向先去填坑。

接上一篇:初探Lambda表達式/Java多核編程【3】Lambda語法與作用域

變量捕獲

當使用匿名內部類並去實現其中的接口時,更多時候我們不會去訪問定義在外部的變量,反而更加傾向於將其寫成類似於靜態方法的一種“函數”。

就如同前文中所舉的鍵提取器、鍵比較器之類的例子,作為單純的行為(如Math類中的那些靜態方法),不需要引入或操作任何外部量就能夠達到目的。

同時在上一篇文章中我們也對Lambda之於外部變量的訪問與繼承有了粗淺的了解,書中這一小節的內容將使我們用更專業的術語來表述這一問題。

DoubleUnaryOperator doubleUnaryOperator = x -> Math.abs(x);
Stream.of(-0.1, 0.2, -0.3, 0.4, -0.5)
        .map(e -> doubleUnaryOperator.applyAsDouble(e))
        .forEach(e -> System.out.println(e));

此段代碼中出現的所有Lambda都有一個特性,即只通過參數與返回值與外部交互:

  • x -> Math.abs(x) 接收x,返回其絕對值
  • e -> doubleUnaryOperator.applyAsDouble(e) 接收e,返回運算結果
  • e -> System.out.println(e) 接收e,無返回值

我們將這種類型的Lambda稱為無捕獲Lambda無狀態Lambda,究其緣由應該是此種Lambda與外部或整個系統狀態無關,不對外部量直接進行捕獲,僅通過參數獲得輸入。

相反地,捕獲Lambda則會訪問外部量。

“捕獲”指保留住Lambda對其外部環境的引用。

同樣在前一篇文章中,我們介紹了Lambda與匿名內部類在訪問外部變量時,都不允許有修改變量的傾向,即若:

final double a  = 3.141592;
double b = 3.141592;

DoubleUnaryOperator anotherDoubleUnaryOperator = x -> {
    a = 2; // ERROR
    b = 3; // ERROR
    return 0.0;
    };

則:

  • 無法改變final量的值
  • 不允許在Lambda表達式中修改使用的(外部)變量

相應的報錯信息:

  • Cannot assign a value to final variable
  • Variable used in lambda expression should be final or effectively final

由是觀之,我們將Lambda的這種變量捕獲行為稱之為值捕獲更加確切。

在實際操作中,如果我們在Lambda體內或匿名內部類中要對外部的某些量進行操作,那麼這種限制也是很容易被規避,因為即使數組是final的(即該數組不能再指向其他數組對象),裡面的值依舊可變。

所以我們只要將那個需要被操作的外部變量包裝進一個數組即可:

final double[] a = {3.141592};
final double[] b = {3.141592};

DoubleUnaryOperator anotherDoubleUnaryOperator = x -> {
    a[0] = 2; // COOL
    b[0] = 3; // ALSO COOL
    return 0.0;
};

也算是一個小技巧,在安卓開發中特別常見。

至於為何庫的設計者如此竭力防止調用者修改外部變量,書中給出的解釋是保證程序的正確性以及性能,很容易想到,如果我們將Lambda傳遞給另一個線程,此時如果Lambda在某一時刻修改了外部變量的值,便很容易引起多線程相關的bug。同時,我們若要解決線程安全問題,就需要給相關的外部變量上鎖或使用volatile關鍵字,導致了計算任務分發至不同線程後的效率問題,又違背了Lambda的初衷。

關於線程安全,書中還出現了以下兩個關鍵詞:

  • 可見性問題
  • 競態條件

看來還需要提升知識水平才能把多線程、高並發的坑給填了,現在還不大能看懂(╯-_-)╯╧╧。

拋開線程不談,Lambda的生命周期可能比使用Lambda的方法調用的周期還要長,因此如果Lambda捕獲的外部變量是可變的,還會引起與局部變量內存洩漏相關的問題。

對於常見的需要修改外部變量的場景:

final int[] sum = {0};
Stream.of(0, 1, 2, 3, 4, 5)
        .forEach(e -> sum[0] += e);

Stream給出了完美的解決方案:

sum[0] = Stream.of(0, 1, 2, 3, 4, 5)
        .mapToInt(e -> Integer.valueOf(e))
        .sum();

也可以是:

sum[0] = Stream.of(0, 1, 2, 3, 4, 5)
        .parallel()
        .mapToInt(e -> Integer.valueOf(e))
        .sum();

總之,面向並行的方式能夠給我們帶來更優越的性能。

小結

Lambda能夠修改字段,不受final之類的的限制:

private static class AClass {
    private String aString = "Animal Farm";

    void aMethod() {
        new Thread(() -> aString = aString.concat(" is good.")).run();
        System.out.println(aString);
    }
}

對字段aString的引用實際上由this.aString解引用而來,其中this充當了不可變局部變量的角色,進而使我們能夠修改aString的值,與將外部變量包裝進數組有異曲同工之妙。

本章代碼:

import java.util.function.DoubleUnaryOperator;
import java.util.stream.Stream;

public class Bar {
    public static void main(String[] args) {
        DoubleUnaryOperator doubleUnaryOperator = x -> Math.abs(x);
        Stream.of(-0.1, 0.2, -0.3, 0.4, -0.5)
                .map(e -> doubleUnaryOperator.applyAsDouble(e))
                .forEach(e -> System.out.println(e));

//        final double a  = 3.141592;
//        double b = 3.141592;
//
//        DoubleUnaryOperator anotherDoubleUnaryOperator = x -> {
//            a = 2;
//            b = 3;
//            return 0.0;
//        };

        final double[] a = {3.141592};
        final double[] b = {3.141592};

        DoubleUnaryOperator anotherDoubleUnaryOperator = x -> {
            a[0] = 2;
            b[0] = 3;
            return 0.0;
        };

        final int[] sum = {0};
        Stream.of(0, 1, 2, 3, 4, 5)
                .forEach(e -> sum[0] += e);
        System.out.println(sum[0]);

        sum[0] = Stream.of(0, 1, 2, 3, 4, 5)
                .parallel()
                .mapToInt(e -> Integer.valueOf(e))
                .sum();
        System.out.println(sum[0]);

        new AClass().aMethod();
    }

    private static class AClass {
        private String aString = "Animal Farm";

        void aMethod() {
            new Thread(() -> aString = aString.concat(" is good.")).run();
            System.out.println(aString);
        }
    }
}

以及運行結果:

0.1
0.2
0.3
0.4
0.5
15
15
Animal Farm is good.



  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved