1. 初识闭包
闭包 是一种函数(或函数对象),它不仅使用自己的参数,还会“记住”创建它时所在环境中的变量。简单说,如果方法内部的 lambda 表达式或匿名类使用了该方法的局部变量,它就成为一个闭包。
通俗示例
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "你好,";
Runnable sayHello = () -> System.out.println(greeting + "世界!");
sayHello.run(); // 输出:你好,世界!
}
}
这里,lambda “捕获”了外部方法中的变量 greeting 并在自身内部使用。这就是闭包!
2. 有效 final 变量:是什么,为什么需要?
在 Java 中,lambda 表达式(以及匿名类)只能使用来自外部方法、被声明为 final 的变量,或那些在初始化后未被修改的变量。这样的变量称为 有效 final。
为什么?
这是因为 lambda 表达式可能会在创建它的方法结束执行之后才被调用。如果变量还能继续改变,就会产生歧义:到底该用变量的哪个版本?为避免这种“惊喜”,Java 要求变量必须不可变(或者至少看起来不可变)。
示例:正确的用法
public static void main(String[] args) {
int number = 42; // number 是有效的 final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
示例:尝试在使用后修改变量
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // 错误:变量 number 必须是 final 或有效 final
r.run();
}
编译器会报错:Variable used in lambda expression should be final or effectively final。
所谓“有效 final”是……变量只被赋值一次,之后不再改变。并不一定要显式写 final,编译器会自行判断。
3. lambda 表达式如何捕获变量?
当你编写使用外部变量的 lambda 时,Java 会把该变量与 lambda 一起“打包”。即使创建该 lambda 的方法已经结束,变量也不会消失——它在闭包内部仍然存在。
示意:lambda “记住”变量
public static Runnable createGreeter(String name) {
// name — 参数,会被 lambda 捕获
return () -> System.out.println("你好," + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("Vasya");
greeter.run(); // 你好,Vasya!
}
这里变量 name 已经不在 main 方法的栈中,但 greeter 仍然“记得”它的值。
内部是如何实现的?
Java 编译器会创建一个专用的辅助对象(也称为“capture/display class”),用于保存所有被捕获的变量。lambda 表达式会变成一个对象,并持有指向这个“容器”的引用。
4. 闭包示例:返回一个使用外部变量的函数
我们来写一个函数,返回一个使用其上下文变量的 lambda:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x — 被 lambda 捕获
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
这里变量 x 已经离开了方法栈,但 lambda 仍可以使用它。
5. 为什么不能修改被捕获的变量?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // 错误!
printer.run();
}
编译器不允许这么做。若允许修改 base,将会不清楚 lambda 中该使用变量的旧值还是新值。因此 Java 禁止修改被 lambda 捕获的局部变量。
lambda 中可以使用什么?
- 初始化后不再改变的局部变量(有效 final)。
- 类的字段(包括 static 和非静态)——它们可以改变,但这属于访问对象状态,而不是捕获局部变量。
6. 与匿名类的对比
在 Java 中,在出现 lambda 表达式之前,可以用匿名类实现闭包:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
规则相同:变量 word 必须是 final 或有效 final。
区别:this 的作用域
- 在匿名类中,this 指向匿名类的实例。
- 在 lambda 表达式中,this 指向外部对象(例如当前类的实例)。
7. 闭包与类字段
如果 lambda 使用的是类的字段,这并不属于严格意义上的“捕获局部变量”——字段始终可访问,并且可以被修改。
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("计数器: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // 计数器: 1
r.run(); // 计数器: 2
}
}
8. Java 中闭包的常见错误与注意点
错误 1:试图修改被 lambda 捕获的变量。 最常见的错误是:在变量被用于 lambda 之后仍尝试修改该局部变量。编译器会提示:Variable used in lambda expression should be final or effectively final。
错误 2:以为变量会被“冻结”。 在 Java 中,当涉及类字段时,被捕获的并不是值的副本,而是对原始字段的访问。如果类字段发生变化,lambda 会看到新值。但在 lambda 中使用的局部变量必须是有效 final。
错误 3:以为 lambda 会为 this 创建新的作用域。 在 lambda 中,this 指向外部对象(容器类)。在匿名类中,this 指向匿名类自身。
错误 4:使用可变对象。 如果你捕获的是对可变对象(例如 List)的引用,即使变量本身是有效 final,你仍然可以在 lambda 内部修改其内容:
public static void main(String[] args) {
java.util.List<String> list = new java.util.ArrayList<>();
Runnable r = () -> list.add("Hello");
r.run();
System.out.println(list); // [Hello]
}
这里变量 list 本身没有变化(我们没有做 list = ...),但它内部所指向的对象内容发生了变化。
GO TO FULL VERSION