Java Lambda 表达式中的变量捕获
最后修改于 2025 年 5 月 25 日
本教程探讨了 Java Lambda 表达式中的变量捕获,这是 Java 函数式编程中的一个基本概念。我们将介绍 lambda 与周围变量交互的规则和实际示例。
Lambda 表达式中的 变量捕获 是指 lambda 使用在其主体之外声明的变量的能力。这些变量被称为 lambda *捕获* 的。Java 对哪些变量可以被捕获以及如何使用它们有明确的规则。
最重要的规则是,在 lambda 中使用但未声明的任何局部变量、形式参数或异常参数必须是 final 的或事实上是 final 的。这确保了 lambda 执行时的可预测行为,尤其是在并发上下文中。
事实上是 final 的 变量是指在初始化后其值永远不会改变的变量。虽然没有用 final 关键字显式声明,但它可以被视为 final,因为它的值在其作用域内保持不变。
这条规则至关重要,因为它防止了 lambda 稍后执行时出现意外行为,例如在不同的线程中或在方法返回后。如果 lambda 可以修改捕获的变量,则可能导致不可预测的结果,尤其是在并发编程场景中。
捕获局部变量
此示例演示了在 lambda 表达式中捕获局部变量。该变量必须事实上是 final 的才能被捕获。
void main() {
final String greeting = "Hello";
Runnable r = () -> System.out.println(greeting + " there!");
r.run();
}
在这里,lambda 捕获了显式声明为 final 的局部变量 'greeting'。Lambda 可以在执行时访问此变量。 如果我们尝试在 lambda 声明后修改 greeting,则会导致编译错误。
$ java Main.java Hello there!
事实上是 Final 的变量
这个例子表明,如果变量事实上是 final 的,则不需要显式地声明为 final。 如果它们没有被修改,编译器会将它们视为 final。
void main() {
int count = 5; // effectively final
Function<Integer, Integer> multiplier = x -> x * count;
System.out.println(multiplier.apply(3));
}
变量 count 事实上是 final 的,因为它在初始化后没有被修改。 Lambda 捕获了这个变量并在乘法中使用它。 尝试在 lambda 之后修改 count 会导致错误。
$ java Main.java 15
实例变量捕获
Lambda 表达式可以捕获实例变量,没有任何限制。 与局部变量不同,实例变量不需要是 final 的或事实上是 final 的。
class Example {
private String prefix = "Result: ";
void process() {
Function<Integer, String> formatter = n -> prefix + n;
System.out.println(formatter.apply(42));
}
}
void main() {
new Example().process();
}
Lambda 捕获实例变量 prefix,即使在 lambda 声明之后也可以修改它。 这是因为实例变量存储在堆中,而局部变量存储在栈中。 Lambda 可以直接访问实例变量,没有任何限制。
Java 中的 Lambda 可以自由地访问和修改实例变量(如 prefix),因为这些变量作为对象状态的一部分存储在堆中。 当 lambda 引用实例变量时,它会隐式捕获封闭对象 (this),允许它随时访问变量的当前值,即使变量在 lambda 声明后发生更改。 例如,如果 prefix 稍后更新,lambda 将在执行时反映最新值,因为它直接引用堆上对象的字段。 这种动态访问是安全的,因为对象(及其字段)只要 lambda 存在就存在。
相比之下,局部变量存储在堆栈上,并且仅在方法执行时存在。 为了防止并发问题或访问无效内存,Java 要求 lambda 将局部变量视为事实上是 final 的(在赋值后保持不变)。 当 lambda 使用局部变量时,Java 会在定义 lambda 时创建其值的副本。 这确保了 lambda 具有一致的快照,即使原始变量的堆栈帧被销毁。 在 lambda 创建后修改局部变量会在复制的值和原始值之间创建不匹配,因此编译器强制执行不变性以保持正确性。
$ java Main.java Result: 42
捕获方法参数
如果方法参数事实上是 final 的,则可以被 lambda 表达式捕获。 此示例演示了在 lambda 中捕获方法参数。
void printFiltered(List<String> list, String word) {
list.stream()
.filter(s -> s.contains(word)) // capturing method parameter
.forEach(System.out::println);
}
void main() {
printFiltered(List.of("apple", "banana", "grape"), "ap");
}
Lambda 捕获方法参数 word,该参数事实上是 final 的。 该参数在 word 操作中用于选择包含单词文本的字符串。 捕获后不能修改该参数。
$ java Main.java apple grape
尝试修改捕获的变量
此示例演示了尝试修改捕获的变量时会发生什么,这违反了事实上是 final 的要求。
void main() {
int counter = 0;
// This lambda would cause a compilation error if uncommented
// Runnable incrementer = () -> counter++; // Error
// counter++; // This would make counter not effectively final
Runnable printer = () -> System.out.println("Counter: " + counter);
printer.run();
}
注释掉的代码显示了修改捕获变量的尝试,这将导致编译错误。 工作示例仅读取变量,只要变量保持事实上是 final 的,这是允许的。
$ java Main.java Counter: 0
捕获多个变量
Lambda 可以从其封闭范围捕获多个变量,只要所有捕获的变量都事实上是 final 的。
void main() {
String name = "Alice";
int age = 30;
String format = "%s is %d years old";
Supplier<String> info = () -> String.format(format, name, age);
System.out.println(info.get());
}
此 lambda 捕获三个变量:name、age 和 format。 所有这些变量都事实上是 final 的,并且可以在 lambda 主体中使用。 Lambda 结合这些值来创建一个格式化的字符串。
$ java Main.java Alice is 30 years old
嵌套 Lambda 中的捕获
此示例演示了嵌套 lambda 表达式中的变量捕获。 相同的事实上是 final 的规则适用于嵌套 lambda 捕获的变量。
void main() {
final int base = 10;
Function<Integer, Function<Integer, Integer>> adderCreator =
x -> y -> x + y + base; // nested lambda
Function<Integer, Integer> addFive = adderCreator.apply(5);
System.out.println(addFive.apply(3)); // 5 + 3 + 10 = 18
}
外部 lambda 捕获 'base',内部 lambda 从外部 lambda 捕获 x。 两个变量都必须事实上是 final 的。 结果是一个以柯里化方式将三个数字相加的函数。
$ java Main.java 18
来源
本教程探讨了 Java lambda 表达式中的变量捕获。 我们已经看到 lambda 如何捕获局部变量、实例变量和方法参数,但重要的是,局部变量必须事实上是 final 的。 这些规则有助于确保函数式编程上下文中可预测的行为。
作者
列出所有Java教程。