Lambda 表達式是 Java 8 引入的函數式編程核心特性。本文聚焦變量捕獲機制,詳細解析無捕獲/無狀態 Lambda與捕獲 Lambda的區別、值捕獲語義(effectively final 限制)、規避技巧(final 陣列包裝)、線程安全風險(可見性、競態條件)、實例欄位捕獲機制,並提供完整可執行範例與最佳實踐建議。
僅通過參數與返回值與外部交互,不引用任何外部變量。
DoubleUnaryOperator op = x -> Math.abs(x); // 只用參數 x stream.map(e -> op.applyAsDouble(e)); // 只用參數 e stream.forEach(e -> System.out.println(e)); // 只用參數 e
特點:可序列化、線程安全、無閉包開銷、適合並行流。
訪問外部區域變量(區域變量、參數、實例欄位、靜態欄位)。
int factor = 10; Function<Integer, Integer> multiply = x -> x * factor; // 捕獲 factor
Java 要求捕獲的區域變量必須是 final 或 effectively final(編譯期可推導為最終賦值一次,未顯式宣告 final)。
final double a = 3.14;
double b = 3.14;
DoubleUnaryOperator op = x -> {
a = 2; // 編譯錯誤:Cannot assign a value to final variable
b = 3; // 編譯錯誤:Variable used in lambda expression should be final or effectively final
return 0.0;
};
本質:Lambda 捕獲的是變量的值(值捕獲),而非變量本身。這保證了 Lambda 執行時看到的值與捕獲時刻一致。
將可變狀態封裝在 final 陣列或 AtomicReference 等容器中,容器引用不變,內部值可變。
final double[] a = {3.14};
final double[] b = {3.14};
DoubleUnaryOperator op = x -> {
a[0] = 2; // 合法
b[0] = 3; // 合法
return 0.0;
};
Android 開發中極常見(如 final TextView[] tv = {null};)。
volatile,違背 Lambda 無副作用、易並行的初衷。Lambda 訪問實例欄位實際上是捕獲 this 引用(effectively final),再通過 this.field 存取,因此可以修改實例欄位。
private static class AClass {
private String aString = "Animal Farm";
void aMethod() {
new Thread(() -> aString = aString.concat(" is good.")).run();
System.out.println(aString); // 輸出: Animal Farm is good.
}
}
等價於 final AClass thisRef = this; thisRef.aString = ...。
需求:累加 0~5
// 副作用寫法(不推薦)
final int[] sum = {0};
IntStream.rangeClosed(0, 5).forEach(e -> sum[0] += e);
// 函數式寫法(推薦,無副作用、可並行)
int sum = IntStream.rangeClosed(0, 5).sum();
// 或並行
int sum = IntStream.rangeClosed(0, 5).parallel().sum();
函數式風格天然線程安全、可優化並行、程式碼更簡潔。
import java.util.function.DoubleUnaryOperator;
import java.util.stream.Stream;
public class LambdaCaptureDemo {
public static void main(String[] args) {
// 無捕獲 Lambda
DoubleUnaryOperator abs = x -> Math.abs(x);
Stream.of(-0.1, 0.2, -0.3, 0.4, -0.5)
.map(e -> abs.applyAsDouble(e))
.forEach(e -> System.out.println(e));
// 捕獲 final 陣列
final double[] a = {3.141592};
final double[] b = {3.141592};
DoubleUnaryOperator modify = x -> { a[0] = 2; b[0] = 3; return 0.0; };
// 累加對比
final int[] sumArr = {0};
Stream.of(0,1,2,3,4,5).forEach(e -> sumArr[0] += e);
System.out.println("副作用累加: " + sumArr[0]);
int sumFunc = Stream.of(0,1,2,3,4,5).parallel().mapToInt(e -> e).sum();
System.out.println("函數式並行累加: " + sumFunc);
// 實例欄位捕獲
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.
| 場景 | 建議 |
|---|---|
| 純計算轉換 | 使用無捕獲 Lambda + 方法引用 |
| 需讀取外部配置 | 捕獲 effectively final 區域變量或實例欄位 |
| 需在 Lambda 內修改狀態 | 優先改用函數式 API;必須時用 final 陣列/Atomic* 包裝 |
| 多執行緒環境 | 避免捕獲可變共享狀態;用 ThreadLocal、CompletableFuture 隔離 |
| 序列化需求 | 無捕獲 Lambda 可序列化;捕獲 Lambda 需所有捕獲變量可序列化 |