💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 闭包 在上一节的`ProduceFunction.java`中,我们从方法中返回 Lambda 函数。 虽然过程简单,但是有些问题必须再回过头来探讨一下。 **闭包**(Closure)一词总结了这些问题。 它非常重要,利用闭包可以轻松生成函数。 考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决,那问题将变得非常棘手。 能够解决这个问题的语言被称为**支持闭包**,或者叫作在词法上限定范围( 也使用术语*变量捕获*)。Java 8 提供了有限但合理的闭包支持,我们将用一些简单的例子来研究它。 首先,下列方法返回一个函数,该函数访问对象字段和方法参数: ~~~java // functional/Closure1.java import java.util.function.*; public class Closure1 { int i; IntSupplier makeFun(int x) { return () -> x + i++; } } ~~~ 但是,仔细考虑一下,`i`的这种用法并非是个大难题,因为对象很可能在你调用`makeFun()`之后就存在了——实际上,垃圾收集器几乎肯定会保留以这种方式被绑定到现存函数的对象\[^5\]。当然,如果你对同一个对象多次调用`makeFun()`,你最终会得到多个函数,它们共享`i`的存储空间: ~~~java // functional/SharedStorage.java import java.util.function.*; public class SharedStorage { public static void main(String[] args) { Closure1 c1 = new Closure1(); IntSupplier f1 = c1.makeFun(0); IntSupplier f2 = c1.makeFun(0); IntSupplier f3 = c1.makeFun(0); System.out.println(f1.getAsInt()); System.out.println(f2.getAsInt()); System.out.println(f3.getAsInt()); } } ~~~ 输出结果: ~~~ 0 1 2 ~~~ 每次调用`getAsInt()`都会增加`i`,表明存储是共享的。 如果`i`是`makeFun()`的局部变量怎么办? 在正常情况下,当`makeFun()`完成时`i`就消失。 但它仍可以编译: ~~~java // functional/Closure2.java import java.util.function.*; public class Closure2 { IntSupplier makeFun(int x) { int i = 0; return () -> x + i; } } ~~~ 由`makeFun()`返回的`IntSupplier`“关住了”`i`和`x`,因此即使`makeFun()`已执行完毕,当你调用返回的函数时`i`和`x`仍然有效,而不是像正常情况下那样在`makeFun()`执行后`i`和`x`就消失了。 但请注意,我没有像`Closure1.java`那样递增`i`,因为会产生编译时错误。代码示例: ~~~java // functional/Closure3.java // {WillNotCompile} import java.util.function.*; public class Closure3 { IntSupplier makeFun(int x) { int i = 0; // x++ 和 i++ 都会报错: return () -> x++ + i++; } } ~~~ `x`和`i`的操作都犯了同样的错误:被 Lambda 表达式引用的局部变量必须是`final`或者是等同`final`效果的。 如果使用`final`修饰`x`和`i`,就不能再递增它们的值了。代码示例: ~~~java // functional/Closure4.java import java.util.function.*; public class Closure4 { IntSupplier makeFun(final int x) { final int i = 0; return () -> x + i; } } ~~~ 那么为什么在`Closure2.java`中,`x`和`i`非`final`却可以运行呢? 这就叫做**等同 final 效果**(Effectively Final)。这个术语是在 Java 8 才开始出现的,表示虽然没有明确地声明变量是`final`的,但是因变量值没被改变过而实际有了`final`同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是`final`的。 如果`x`和`i`的值在方法中的其他位置发生改变(但不在返回的函数内部),则编译器仍将视其为错误。每个递增操作则会分别产生错误消息。代码示例: ~~~java // functional/Closure5.java // {无法编译成功} import java.util.function.*; public class Closure5 { IntSupplier makeFun(int x) { int i = 0; i++; x++; return () -> x + i; } } ~~~ **等同 final 效果**意味着可以在变量声明前加上**final**关键字而不用更改任何其余代码。 实际上它就是具备`final`效果的,只是没有明确说明。 通过在闭包中使用`final`关键字提前修饰变量`x`和`i`, 我们解决了`Closure5.java`中的问题。代码示例: ~~~java // functional/Closure6.java import java.util.function.*; public class Closure6 { IntSupplier makeFun(int x) { int i = 0; i++; x++; final int iFinal = i; final int xFinal = x; return () -> xFinal + iFinal; } } ~~~ 上例中`iFinal`和`xFinal`的值在赋值后并没有改变过,因此在这里使用`final`是多余的。 如果函数式方法中使用的外部局部变量是引用,而不是基本类型的话,会是什么情况呢?我们可以把`int`类型改为`Integer`类型研究一下: ~~~java // functional/Closure7.java // {无法编译成功} import java.util.function.*; public class Closure7 { IntSupplier makeFun(int x) { Integer i = 0; i = i + 1; return () -> x + i; } } ~~~ 编译器非常聪明地识别到变量`i`的值被更改过。 因为包装类型可能被特殊处理过了,所以我们尝试下**List**: ~~~java // functional/Closure8.java import java.util.*; import java.util.function.*; public class Closure8 { Supplier<List<Integer>> makeFun() { final List<Integer> ai = new ArrayList<>(); ai.add(1); return () -> ai; } public static void main(String[] args) { Closure8 c7 = new Closure8(); List<Integer> l1 = c7.makeFun().get(), l2 = c7.makeFun().get(); System.out.println(l1); System.out.println(l2); l1.add(42); l2.add(96); System.out.println(l1); System.out.println(l2); } } ~~~ 输出结果: ~~~ [1] [1] [1, 42] [1, 96] ~~~ 可以看到,这次一切正常。我们改变了**List**的内容却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用`makeFun()`时,其实都会创建并返回一个全新而非共享的`ArrayList`。也就是说,每个闭包都有自己独立的`ArrayList`,它们之间互不干扰。 请**注意**我已经声明`ai`是`final`的了。尽管在这个例子中你可以去掉`final`并得到相同的结果(试试吧!)。 应用于对象引用的`final`关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。 下面我们来看看`Closure7.java`和`Closure8.java`之间的区别。我们看到:在`Closure7.java`中变量`i`有过重新赋值。 也许这就是触发**等同 final 效果**错误消息的原因。 ~~~java // functional/Closure9.java // {无法编译成功} import java.util.*; import java.util.function.*; public class Closure9 { Supplier<List<Integer>> makeFun() { List<Integer> ai = new ArrayList<>(); ai = new ArrayList<>(); // Reassignment return () -> ai; } } ~~~ 上例,重新赋值引用会触发错误消息。如果只修改指向的对象则没问题,只要没有其他人获得对该对象的引用(这意味着你有多个实体可以修改对象,此时事情会变得非常混乱),基本上就是安全的\[^6\]。 让我们回顾一下`Closure1.java`。那么现在问题来了:为什么变量`i`被修改编译器却没有报错呢。 它既不是`final`的,也不是**等同 final 效果**的。因为`i`是外围类的成员,所以这样做肯定是安全的(除非你正在创建共享可变内存的多个函数)。是的,你可以辩称在这种情况下不会发生变量捕获(Variable Capture)。但可以肯定的是,`Closure3.java`的错误消息是专门针对局部变量的。因此,规则并非只是“在 Lambda 之外定义的任何变量必须是`final`的或**等同 final 效果**那么简单。相反,你必须考虑捕获的变量是否是**等同 final 效果**的。 如果它是对象中的字段,那么它拥有独立的生存周期,并且不需要任何特殊的捕获,以便稍后在调用 Lambda 时存在。