在 Kotlin 中,闭包(Closure)是一个非常核心且强大的函数式编程特性。
闭包(Closure)的核心概念是:闭包是指一个函数(Lambda 表达式、匿名函数)能够访问并"记住"其外部作用域中的变量,即使外部作用域已经执行完毕,这些变量依然可以被该函数安全地读取或修改。简单说,就是"函数带着它的环境一起走"。
例子:
fun makeCounter(): () -> Int {
var count = 0
return { count++ } // 这个 Lambda 捕获了外部的 count
}
fun main() {
val counter = makeCounter()
println(counter()) // 0
println(counter()) // 1
println(counter()) // 2
}
makeCounter 执行完后,count 本应销毁,但因为被 Lambda 捕获,它一直存活,每次调用 counter() 都会修改同一个 count。
Kotlin 闭包的特殊点是可以修改捕获的变量:与 Java 的 Lambda 只能捕获 final 变量不同,Kotlin 允许在 Lambda 内部修改外部 var 变量。编译器会通过包装类(如 Ref.IntRef)来实现这一点。
Lambda 是闭包的主要载体:Kotlin 中闭包通常以 Lambda 形式出现,但匿名函数(fun(...) = ...)同样可以形成闭包。
典型应用:函数柯里化
与柯里化的关系:柯里化其实现依赖闭包。每个返回的 Lambda 都"记住"了之前传入的参数:
val sumCurried = { a: Int -> { b: Int -> { c: Int -> a + b + c } } }
内层 Lambda 捕获了 a 和 b,这就是闭包在起作用。
Kotlin 的闭包 = 函数 + 它捕获的外部变量环境。它让函数可以"记住"定义时的上下文,是函数式编程(柯里化、高阶函数、状态封装)的基础机制。Kotlin 的 Lambda 天生支持闭包,并且允许修改捕获的变量,比 Java 更灵活。在许多老牌语言(如 Java 8 之前的匿名内部类,或 Java 的 Lambda)中,闭包只能读取外部的 final(不可变)变量。
闭包(Closure)与纯函数(Pure Function)
严格来说,所有函数在 Kotlin/Python/JS 这类语言里都可以视为闭包,只是捕获的变量集合为空。所以"没有捕获外部变量的函数"就是捕获集合为空的闭包。如果一个函数只依赖传入的参数,不读取也不修改任何外部状态,那么它符合纯函数的特征之一。但要注意:没有捕获外部变量 ≠ 纯函数。纯函数还要求无副作用(不修改外部状态、不做 I/O 等)。一个没有捕获外部变量但内部执行了 println 的函数,仍然不是纯函数。所以只能说"无捕获"是纯函数的常见前提,不能直接划等号。
Java 不支持在 Lambda 或匿名内部类中修改外部局部变量,根本原因在于它的实现机制是 "按值捕获"(capture-by-value),而非按引用捕获。为了掩盖这种机制带来的语义矛盾,Java 强制要求被捕获的变量必须是 final 或"效果上 final"(effectively final)的。Java 的局部变量存储在栈(Stack) 中,方法执行结束,栈帧销毁,局部变量也就随之消失。但 Lambda 或匿名内部类创建的对象可能存活得更久(例如被传入线程池或作为回调保存)。
如果允许你在 Lambda 内部修改这个变量,修改的只是副本,外部的原始变量完全不受影响;反过来,外部修改了原始变量,内部副本也不会更新。这会让开发者产生严重的误解:明明代码看起来是在操作同一个变量,实际行为却完全割裂。为了避免这种"看起来能改,实际改了也没用"的陷阱,Java 语言规范直接在编译期禁止了这种操作:只要一个局部变量被 Lambda 或内部类捕获,它就必须是 final 的(或者事实上的 final,即赋值后不再改变)。这样一来,变量值固定不变,副本和原始值永远一致,就不会出现不一致的困惑了。
更深层的设计考量:线程安全。除了实现上的限制,Java 语言设计者也有意避免引入"可变的捕获变量"。OpenJDK 的邮件列表讨论中曾提到,像下面这种代码在语义上是"本质上串行"的:
int sum = 0;
list.forEach(e -> { sum += e.size(); }); // Java 禁止这样写
如果允许在 Lambda 里修改外部局部变量,这段代码在多线程环境下极易产生竞态条件(race condition)。要安全地支持这种写法,就需要复杂的同步机制或线程限制检查,这被认为"带来的麻烦远多于解决的问题"。因此,Java 选择从语言层面禁止,引导开发者使用更安全的方式(如用数组或 AtomicInteger 作为容器来绕过限制)。
Java 不支持修改捕获的局部变量,不是技术上做不到(Kotlin、Scala 在同为 JVM 的环境下就支持),而是语言设计上的一种保守取舍:用"按值复制 + 强制不可变"来保证语义的简单和一致,同时避免引入潜在的并发风险。在 Kotlin 里能修改,是因为 Kotlin 编译器在底层用 Ref 包装类模拟了"按引用"的效果,但这在 Java 的设计哲学里是不被接受的。
Kotlin 允许在 Lambda 中修改捕获的局部变量,但这本身并不提供任何线程安全保障。它只是通过编译器生成的 Ref 包装类绕过了 Java 的语法限制,把"能不能改"的问题解决了,却没有解决"多线程改会不会出错"的问题。Kotlin 编译器会把被捕获的可变变量(var)包装进一个 Ref 对象(如 IntRef、ObjectRef)。这个 Ref 实例本身是 final 的,所以可以被 Lambda 捕获;变量真正的值存在 Ref 内部的字段里,Lambda 通过修改这个字段来实现"修改外部变量"。
总结
Java 的闭包受到了严格的限制。Java 选择从语言层面禁止修改捕获的局部变量,迫使开发者显式面对并发问题(比如用 AtomicInteger 或 int\[\] 容器)。Kotlin 把选择权交还给你:你可以写得更方便,但同步责任也随之转移到了你身上。Kotlin 官方文档在讨论协程共享可变状态时,也明确需要使用 Mutex、withContext 限制到单线程,或改用 Atomic 类型来保证正确性。如果只是单线程使用(比如在 forEach 里累加),完全没问题。如果在多线程或协程并发环境下修改捕获的 var,需要自己加锁(synchronized/Mutex)、使用原子类,或者改用 Channel/Flow 等更安全的并发原语。Kotlin 给了你修改的语法,但没有给线程安全。