A closure is a callable object that retains information from the scope where it was created.
闭包是一个可调用对象,它保留了来自于创建该对象的作用域的信息。
- 本文仅以一种闭包形式为例。交流探讨,如有误请批评指正。
Java规定:闭包函数使用的局部变量必须是final或者effectively final ( 等效 final ) 的。但是,从直观上看,即使在方法体内改了局部变量,也不像能导致什么谬误的样子。所以,这个final的规矩让人心生疑惑。
- 先po代码(来自《On Java 8》):
// lambda使用的局部变量必须是final或等效final...
// 基本类型
class Closure6 {
IntSupplier makeFun(int x) { // IntSupplier接口中只有一个方法getAsInt(),无参,返回值类型int.
int i = 0;
i++;
x++;
// return () -> x + i; // 编译器报错: Variables in lambda expressions must be final or effectively final.
// 即:lambda表达式中的变量必须是final 或者 effectively final.
// 不报错的做法:
final int iFinal = i; // final关键字在这里很多余.
final int xFinal = x; // 因为这两个变量赋值后没有做任何更改,是等效final的.
return () -> xFinal + iFinal;
}
}
// 对象引用
class Closure9 {
Supplier<List<Integer>> makeFun() { // Supplier接口中只有一个方法get(),无参,返回<>中类型,此处即 List<Integer>.
List<Integer> ai = new ArrayList<>();
// ai = new ArrayList<>(); // Reassignment
return () -> ai; // 若前一行不注释, 则这里报错.
}
}
class Closure1 {
int i;
IntSupplier makeFun(int x) {
return () -> x + i++; // 使用类成员变量时,可以更改而不报错。
}
}
这两个报错展示了文章开头的规则。那么这是为啥嘞?
-
我们已经知道,一个局部变量在它的作用域之外是不存在的,那么把它放在lambda表达式里并返回至作用域外,看起来好像需要java的“特许”。
-
根据这个猜想,我原以为:java出于某种善意避免程序员犯错,所以立下了规矩,即:被lambda“捕捉”的局部变量必须是final或者"等效final"的,来避免变量被重复访问修改,从而导致confusion(混淆)。
-
但遗憾的是,我高估了java的“善意”了,通过RednaxelaFX大佬的解析习得:实际上java只是copy了一份value到表达式中(capture-by-value),而不是capture-by-reference(比如C#把被捕获的局部变量“提升”(hoist)到对象里,使变量依托于对象存在),所以lambda使用的变量跟最初定义的局部变量(包括基本类型标识符和对象引用)彻底脱钩了。lambda表达式访问的只是一个副本。
-
而为了掩饰这种“简单粗暴”导致的“变量不能再次访问”,java干脆告诉你:“变量定义之后就别改了哈”,好像不能再次访问的原因仅仅是这个规定而已。但实际上由于java并没有实现capture-by-reference,因此对于一个离开了作用域就不复存在的局部变量,你即便想改,也改不了。所以,这个规定好像脱裤子放屁,掩耳盗铃了。
-
被lambda使用的类成员变量则没有这样的束缚,这是因为,(非static)成员变量只依赖于对象存在。而这个对象以一种看不见的“this”方式存在于lambda表达式的参数列表中,所以垃圾回收器不会去伤害这个对象(这个原则对普通函数同样适用)。故,类成员变量不需要被主动capture(捕获),这也印证了代码在Closure1处为何不报错,因为变量 i 始终有参数中隐含的this对象可以依赖。