Java 8 之 lambda 變數作用域
通常,我們希望能夠在lambda表示式的閉合方法或類中訪問其他的變數,例如:
package java8test; public class T1 { public static void main(String[] args) { repeatMessage("Hello", 20); } public static void repeatMessage(String text,int count){ Runnable r = () -> { for(int i = 0; i < count; i++){ System.out.println(text); Thread.yield(); } }; new Thread(r).start(); } }
注意看lambda表示式中的變數count和text,它們並沒有在lambda表示式中被定義,而是方法repeatMessage的引數變數。如果你思考一下,就會發現這裡有一些隱含的東西。lambda表示式可能會在repeatMessage返回之後才執行,此時引數變數已經消失了。如果保留text和count變數會怎樣呢?
為了理解這一點,我們需要對lambda表示式有更深入的理解。一個lambda表示式包括三個部分:
- 一段程式碼
- 引數
- 自由變數的值,這裡的“自由”指的是那些不是引數並且沒有在程式碼中定義的變數。
在我們的示例中,lambda表示式有兩個自由變數,text和count。資料結構表示lambda表示式必須儲存這兩個變數的值,即“Hello”和20。我們可以說,這些值已經被lambda表示式捕獲了(這是一個技術實現的細節。例如,你可以將一個lambda表示式轉換為一個只含一個方法的物件,這樣自由變數的值就會被複制到該物件的例項變數中)。
注意:含有自由變數的程式碼塊才被稱之為“閉包(closure)”。在Java中,lambda表示式就是閉包。事實上,內部類一直都是閉包。Java8中為閉包賦予了更吸引人的語法。
如你所見,lambda表示式可以捕獲閉合作用域中的變數值。在java中,為了確保被捕獲的值是被良好定義的,需要遵守一個重要的約束。在lambda表示式中,被引用的變數的值不可以被更改。例如,下面這個表示式是不合法的:
public static void repeatMessage(String text,int count){ Runnable r = () -> { while(count > 0){ count--; //錯誤,不能更改已捕獲變數的值 System.out.println(text); Thread.yield(); } }; new Thread(r).start(); }
做出這個約束是有原因的。更改lambda表示式中的變數不是執行緒安全的。假設有一系列併發的任務,每個執行緒都會更新一個共享的計數器。
int matches = 0; for(Path p : files) new Thread(() -> {if(p中包含某些屬性) matches++;}).start(); //非法更改matches的值
如果這段程式碼是合法的,那麼會引起十分糟糕的結果。自增操作matches++不是原子操作,如果多個執行緒併發執行該自增操作,天曉得會發生什麼。
不要指望編譯器會捕獲所有併發訪問錯誤。不可變的約束只作用在區域性變數上,如果matches是一個例項變數或者閉合類的靜態變數,那麼不會有任何錯誤被報告出來即使結果同樣未定義。同樣,改變一個共享物件也是完全合法的,即使這樣並不恰當。例如:
List<Path> matches = new ArrayList<>(); for(Path p: files) //你可以改變matches的值,但是在多執行緒下是不安全的 new Thread(() -> {if(p中包含某些屬性) matches.add(p);}).start();
注意matches是“有效final”的(一個有效的final變數被初始化後,就永遠不會再被賦一個新值的變數)。在我們的示例中,matches總是引用同一個ArrayList物件,但是,這個物件是可變的,因此是執行緒不安全的 。如果多個執行緒同時呼叫add方法,結果將無法預測。
lambda表示式的方法體與巢狀程式碼塊有著相同的作用域。因此它也適用同樣的命名衝突和遮蔽規則。在lambda表示式中不允許宣告一個與區域性變數同名的引數或者區域性變數。
Path first = Paths.get("/usr/bin"); Comparator<String> comp = (first,second) -> Integer.compare(first.length(),second.length()); //錯誤,變數first已經定義了
在一個方法裡,你不能有兩個同名的區域性變數,因此,你也不能在lambda表示式中引入這樣的變數。
當你在lambda表示式中使用this關鍵字,你會引用建立該lambda表示式的方法的this引數,以下面的程式碼為例:
public class Application{ public void doWork(){ Runnable runner = () -> {....;System.out.println(this.toString());......}; } }
表示式this.toString()會呼叫Application物件的toString()方法,而不是Runnable例項的toString()方法。在lambda表示式中使用this,與在其他地方使用this沒有什麼不同。lambda表示式的作用域被巢狀在doWork()方法中,並且無論this位於方法的何處,其意義都是一樣的。
相關文章
- java中變數的作用域Java變數
- JavaScript之變數及作用域JavaScript變數
- JavaScript變數作用域之殤JavaScript變數
- 變數作用域變數
- Java基礎06:變數、常量、作用域Java變數
- JS變數作用域JS變數
- SCSS 變數作用域CSS變數
- golang變數作用域Golang變數
- JAVA基礎之八-方法變數作用域和編譯器Java變數編譯
- 函式(三)作用域之變數作用域、函式巢狀中區域性函式作用域、預設值引數作用域函式變數巢狀
- java基礎06-變數、常量、作用域Java變數
- JAVA基礎6-變數、常量、作用域Java變數
- python變數與變數作用域Python變數
- PL/SQL變數作用域SQL變數
- lisp 變數的作用域Lisp變數
- LoadRunner變數作用域變數
- C# 變數作用域C#變數
- JS 底蘊之 變數、作用域和垃圾回收JS變數
- 變數物件與作用域鏈變數物件
- JavaScript中變數和作用域JavaScript變數
- Go 語言變數作用域Go變數
- JavaScript 變數的作用域鏈JavaScript變數
- Java8 Lambda 之 Collection StreamJava
- javascript中的作用域(全域性變數和區域性變數)JavaScript變數
- 現代 JavaScript 的變數作用域JavaScript變數
- Go語言中的變數作用域Go變數
- 變數、作用域與記憶體變數記憶體
- Shell變數的作用域問題變數
- ES6(二: 變數作用域)變數
- js中變數作用域問題JS變數
- 變數的作用域--js閉包變數JS
- 理解 Javascript 中變數的作用域JavaScript變數
- Day08-常量、變數、作用域變數
- 【譯】java8之lambda表示式Java
- golang變數作用域問題-避免使用全域性變數Golang變數
- Go 中的動態作用域變數Go變數
- ES6 變數作用域總結變數
- 注意for迴圈中變數的作用域變數