在 Java 中,lambda 表示式要求捕獲的區域性變數是 final
或者 effectively final(“實際上是 final”)的,即在宣告後沒有被重新賦值。這一限制是由 lambda 表示式的設計原理和作用域管理機制決定的,目的是確保程式碼行為的一致性和執行緒安全。下面詳細解釋這個原因。
1. Lambda 表示式中的“變數捕獲”機制
Java 的 lambda 表示式可以訪問外部作用域中的變數(包括區域性變數和例項變數)。這種訪問叫做變數捕獲。當 lambda 表示式捕獲某個區域性變數時,它實際上並不是直接引用這個變數,而是捕獲該變數的一個副本:
- 例項變數或類變數的引用可以直接在 lambda 表示式中使用,因為它們的生命週期和 lambda 表示式相同。
- 區域性變數則不同,因為它們是棧上分配的,生命週期僅在當前方法呼叫期間有效。而 lambda 表示式可能會在方法執行結束後還繼續存在,比如作為執行緒、非同步回撥等情況。
因此,lambda 表示式必須捕獲區域性變數的值,而不能直接訪問它們,以避免生命週期衝突。
2. 為什麼要求區域性變數是 final
或者 effectively final
為了確保 lambda 表示式的捕獲變數值在其生命週期內保持不變,Java 要求捕獲的區域性變數必須是 final
或者 effectively final。這有助於在以下方面保證一致性:
-
不可變性:Java 透過
final
限制確保區域性變數在 lambda 表示式中是不可變的,防止 lambda 表示式在不同的執行時間看到不同的變數值。例如,lambda 表示式可能會在非同步執行緒中被執行,如果 lambda 捕獲的變數值可以被修改,那麼可能會出現併發修改問題。 -
變數副本的穩定性:由於 lambda 表示式捕獲的是區域性變數的副本,不是原始變數,如果允許它們在外部方法中被修改,會導致 lambda 捕獲的值和外部變數的值不一致,進而引發意料之外的行為。例如:
int count = 0; Runnable r = () -> System.out.println(count); count++; // 會導致編譯錯誤,因為 count 不是 final 的
這裡
count++
的賦值操作會讓count
不再是 effectively final,從而無法被 lambda 捕獲。
3. Lambda 表示式的執行緒安全需求
在多執行緒環境下,如果 lambda 表示式捕獲的區域性變數是可變的,不同執行緒可能會對同一變數進行併發修改,這會帶來潛在的執行緒安全問題。透過限制 lambda 表示式只能訪問不可變的 final
或 effectively final 的變數,Java 避免了可能的併發修改問題,確保了 lambda 表示式行為的執行緒安全性。
4. 與匿名內部類的區別
Java 中匿名內部類在訪問外部區域性變數時也有類似的要求,即捕獲的區域性變數必須是 final
或 effectively final。但 lambda 表示式相較於匿名內部類,具有更嚴格的不可變性要求,確保了 lambda 表示式的函數語言程式設計特性,並使其更適合在併發場景下使用。
示例
以下是一個演示 lambda 表示式如何捕獲外部變數的示例:
public class LambdaExample {
public static void main(String[] args) {
int number = 5; // effectively final 變數
Runnable runnable = () -> System.out.println(number);
number++; // 此行會導致編譯錯誤,因為 number 不是 effectively final
}
}
在這個示例中,如果嘗試在 lambda 表示式外部對 number
進行修改,編譯器會報錯,因為這樣 number
就不再是 effectively final。如果將 number++
行刪除,則 number
是 effectively final 的,可以被 lambda 表示式捕獲和使用。
總結
Java 要求 lambda 表示式捕獲的區域性變數是 final
或 effectively final 的,原因在於:
- 保證不可變性:lambda 表示式捕獲的是變數的副本,不能改變,避免 lambda 中的行為隨著外部變數的變化而變化。
- 確保執行緒安全:區域性變數一旦是 final 或 effectively final 的,避免了 lambda 中對該變數的併發修改問題。
- 避免生命週期衝突:區域性變數的生命週期僅在方法呼叫期間,而 lambda 表示式可能在方法結束後仍然存在,需要捕獲值的副本而不是引用。
這樣,lambda 表示式可以在更廣泛的上下文和執行緒環境中安全使用,同時簡化了編譯器的實現。
然而 Java 中並沒有自帶只可以賦值一次並且賦值後不能再更改的容器,所以安全起見,可以自己實現一個這樣的容器,如下:
Java 原生庫中並沒有專門的工具類來包裝一個只能被 set
一次的值。不過,我們可以透過建立一個自定義的“單賦值容器”類來實現這個功能。這類容器允許我們對值進行一次 set
操作,一旦設定後,再次 set
將丟擲異常,從而確保該值在 lambda 表示式中被捕獲後不會再被修改。
以下是實現該功能的自定義工具類示例:
自定義 SingleAssignmentContainer
工具類
public class SingleAssignmentContainer<T> {
private T value;
private boolean isSet = false;
// 建構函式,設定值時初始化
private SingleAssignmentContainer() {}
// 靜態工廠方法
public static <T> SingleAssignmentContainer<T> create() {
return new SingleAssignmentContainer<>();
}
// 只允許設定一次的 set 方法
public void set(T value) {
if (isSet) {
throw new IllegalStateException("Value has already been set and cannot be changed.");
}
this.value = value;
isSet = true;
}
// 獲取值
public T get() {
if (!isSet) {
throw new IllegalStateException("Value has not been set yet.");
}
return value;
}
// 檢查值是否已設定
public boolean isSet() {
return isSet;
}
}
使用示例
以下示例展示瞭如何在 lambda 表示式中使用 SingleAssignmentContainer
來捕獲和設定一個只能被賦值一次的變數:
public class LambdaExample {
public static void main(String[] args) {
SingleAssignmentContainer<Integer> container = SingleAssignmentContainer.create();
// 在 lambda 表示式中捕獲並設定值
Runnable runnable = () -> {
if (!container.isSet()) {
container.set(42);
System.out.println("Value set to: " + container.get());
} else {
System.out.println("Value is already set to: " + container.get());
}
};
runnable.run(); // 輸出: Value set to: 42
runnable.run(); // 輸出: Value is already set to: 42
// 試圖在另一個地方重新設定值會丟擲異常
try {
container.set(100); // 丟擲異常
} catch (IllegalStateException e) {
System.out.println(e.getMessage()); // 輸出: Value has already been set and cannot be changed.
}
}
}
實現原理
SingleAssignmentContainer
類的set()
方法只允許在值未被設定時進行賦值,第一次賦值後會將isSet
標誌設為true
。- 再次呼叫
set()
時,如果isSet
為true
,則丟擲IllegalStateException
異常。 get()
方法只有在值已經設定的情況下可以安全呼叫,如果尚未設定則丟擲異常。
使用場景
這種包裝類特別適合在 lambda 表示式中使用,比如在併發操作中共享一個只能初始化一次的變數,或者在程式碼塊中捕獲外部變數,確保其值只會被設定一次,防止意外更改。