3 回答

TA貢獻1875條經驗 獲得超3個贊
Lambda 表達式的工作方式類似于回調。在將它們傳遞到代碼中的那一刻,它們“存儲”了它們操作所需的任何外部值(或引用)(就好像這些值在函數調用中作為參數傳遞一樣。這只是對開發人員隱藏)。k在您的第一個示例中,您可以通過存儲到單獨的變量(如 d)來解決此問題:
for (int k = 0; k < 10; k++) {
final int d = k
new Thread(() -> System.out.println(d)).start();
}
實際上final意味著,在上面的示例中,您可以省略“final”關鍵字,因為d它實際上是最終的,因為它在其范圍內從未更改。
for循環的操作方式不同。它們是迭代代碼(與回調相反)。它們在各自的范圍內工作,并且可以使用自己堆棧上的所有變量。這意味著,for循環的代碼塊是外部代碼塊的一部分。
至于您突出顯示的問題:
增強的for循環不能使用常規索引計數器操作,至少不能直接操作。增強for的循環(在非數組上)創建一個隱藏的迭代器。您可以通過以下方式對此進行測試:
Collection<String> mySet = new HashSet<>();
mySet.addAll(Arrays.asList("A", "B", "C"));
for (String myString : mySet) {
if (myString.equals("B")) {
mySet.remove(myString);
}
}
上面的示例將導致 ConcurrentModificationException。這是由于迭代器注意到底層集合在執行期間發生了變化。但是,在您的示例中,外部循環創建了一個“有效的最終”變量arg,可以在 lambda 表達式中引用,因為該值是在執行時捕獲的。
防止捕獲“非有效最終”值或多或少只是 Java 中的一種預防措施,因為在其他語言(例如 JavaScript)中,這會有所不同。
所以編譯器理論上可以翻譯你的代碼,捕獲值,然后繼續,但它必須以不同的方式存儲那個值,你可能會得到意想不到的結果。因此,為 Java 8 開發 lambdas 的團隊正確地排除了這種情況,通過異常阻止它。
如果您需要更改 lambda 表達式中的外部變量的值,您可以聲明一個單元素數組:
String[] myStringRef = { "before" };
someCallingMethod(() -> myStringRef[0] = "after" );
System.out.println(myStringRef[0]);
或使用AtomicReference<T>使其成為線程安全的。但是,對于您的示例,這可能會返回“之前”,因為回調很可能在 println 執行之后執行。

TA貢獻1887條經驗 獲得超5個贊
在增強的 for 循環中,每次迭代都會初始化變量。來自Java 語言規范(JLS)的§14.14.2 :
...
當執行增強for語句時,局部變量在循環的每次迭代中被初始化為數組的連續元素或Iterable由表達式產生。增強語句的確切含義for通過翻譯成基本for語句給出,如下:
如果Expression的類型是 的子類型Iterable,則翻譯如下。
如果Expression的類型是Iterable<X>某個類型參數的子類型X,則令I為類型java.util.Iterator<X>;否則,I設為原始類型java.util.Iterator。
增強for語句等價于for以下形式的基本語句:
for (I #i = Expression.iterator(); #i.hasNext(); ) {
{VariableModifier} TargetType Identifier =
(TargetType) #i.next();
Statement
}
...
否則,表達式必須具有數組類型,T[]。
讓L1 ... Lm是緊接在增強for語句之前的(可能為空的)標簽序列。
增強for語句等價于for以下形式的基本語句:
T[] #a = Expression;
L1: L2: ... Lm:
for (int #i = 0; #i < #a.length; #i++) {
{VariableModifier} TargetType Identifier = #a[#i];
Statement
}
...
換句話說,您的增強 for 循環等效于:
ArrayList<Integer> listOfInt = new ArrayList<>();
// add elements...
for (Iterator<Integer> itr = listOfInt.iterator(); itr.hasNext(); ) {
Integer arg = itr.next();
new Thread(() -> System.out.println(arg)).start();
}
由于每次迭代都會初始化變量,因此它實際上是最終的(除非您在循環內修改變量)。
相反,基本 for 循環中的變量(k在您的情況下)被初始化一次并在每次迭代時更新(如果存在“ ForUpdate ”,例如k++)。有關詳細信息,請參閱JLS 的§14.14.1。由于變量更新,每次迭代都不是最終的,也不是有效的最終。
JLS 的§15.27.2規定并解釋了對最終或有效最終變量的需求:
...
任何使用但未在 lambda 表達式中聲明的局部變量、形式參數或異常參數都必須聲明final或有效地最終確定(第 4.12.4 節),否則在嘗試使用時會發生編譯時錯誤。
任何使用但未在 lambda 主體中聲明的局部變量必須在 lambda 主體之前明確分配(第 16 節(Definite Assignment)),否則會發生編譯時錯誤。
變量使用的類似規則適用于內部類的主體(第 8.1.3 節)。對有效最終變量的限制禁止訪問動態變化的局部變量,這些變量的捕獲可能會引入并發問題。與final限制相比,它減輕了程序員的文書負擔。
對有效最終變量的限制包括標準循環變量,但不包括增強for循環變量,它們對于循環的每次迭代都被視為不同的(第 14.14.2 節)。
...
最后一句話甚至明確提到了基本 for 循環變量和增強型 for 循環變量之間的區別。

TA貢獻1853條經驗 獲得超18個贊
其他回復很有幫助,但他們似乎沒有直接解決問題并明確回答。
在您的第一個示例中,您嘗試k從 lambda 表達式進行訪問。這里的問題是k隨著時間的推移改變它的值(k++在每次循環迭代之后調用)。Lambda 表達式確實捕獲了外部引用,但它們需要被標記為final或“有效地最終”(即,將它們標記為final仍會產生有效代碼)。這是為了防止并發問題;在您創建的線程運行時,k可能已經擁有一個新值。
另一方面,在您的第二個示例中,您正在訪問的變量是arg,它會在增強的 for 循環的每次迭代中重新初始化(與上面的示例相比,k僅更新),因此您正在創建一個完全每次迭代的新變量。順便說一句,您還可以將增強型 for 循環的迭代變量顯式聲明為final:
for (final Integer arg : listOfInt) {
new Thread(() -> System.out.println(arg)).start();
}
這可確保在arg您創建的線程運行時值引用不會更改。
添加回答
舉報