初探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的這種變量捕獲行為稱之為值捕獲更加確切。
在實際操作中,如果我們在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.