本节摘要:闭包是把代码块变成可传递、可存储、可组合的对象,是 Groovy 从面向对象走向函数式的一等公民。本节讲清闭包的本质(groovy.lang.Closure 实例)、作用域解析的 this/owner/delegate 三重奏、柯里化与高阶应用,以及 GDK 集合方法带来的声明式革命。这是全书最值得多花时间的一节。
阅读完本节,你应当能够:
it 隐式参数与隐式返回机制。this、owner、delegate 三个作用域概念,并能通过 resolveStrategy 控制解析顺序。curry、rcurry、ncurry 做部分应用,用闭包组合构建处理管道。想一想 Java 里的排序。想按自定义规则排一个列表,你得写一个 Comparator 匿名内部类,实现 compare,忍受那一堆样板。想过滤、映射、分组,每个都要类似的仪式。问题出在哪?出在"行为"不是一等公民——你不能把一个逻辑块直接传给方法,必须先包一层类。
闭包解决了这个根本问题:它让代码块本身成为可传递、可存储、可修改的对象。在 Groovy 里,{ it * 2 } 就是一个对象,可以赋给变量、传进方法、放进集合、甚至返回出来。这一下,很多"为传行为而造的类"都消失了。更重要的是,它开启了函数式思维:处理集合时,你描述"做什么",而不是"怎么循环"。
Groovy 闭包是 groovy.lang.Closure 类的实例。这一点和 Java 8 的 Lambda 有本质区别:Java Lambda 通常编译成 invokedynamic 的匿名函数,而 Groovy 闭包是一个拥有状态、方法和属性的真实对象。它因此能捕获外部作用域变量(包括局部变量,不必是 final)、能被传递和存储、能在运行时修改行为。
def numbers = [1, 2, 3] numbers.each { println it * 2 } // it 是隐式参数 def add = { a, b -> a + b } // 多参数需显式声明 println add(2, 3) // 5 def lastLine = { 42 } // 隐式返回最后一行
底层实现上,闭包被编译为继承 Closure 的类,因此可以访问外部局部变量——这打破了 Java 匿名内部类对局部变量必须 effectively final 的限制。但也带来了风险:闭包可能意外依赖可变外部状态,并发下会出线程安全问题。原则:闭包尽量无状态,或只捕获不可变数据。
这是最容易混淆、也最重要的概念。三个关键词分别指向不同对象:
this:定义闭包所在的外部类实例,永不改变。owner:定义当前闭包的外部闭包或类。闭包定义在类里时 owner 等于 this;嵌套闭包里 owner 指向外层闭包。delegate:默认指向 owner,但可以被显式修改——这是构建 DSL 的核心机制。当闭包内部引用一个变量时,Groovy 按 resolveStrategy 决定查找顺序。默认是 OWNER_FIRST(先 owner 后 delegate);设为 DELEGATE_FIRST 则优先访问委托对象。设置 delegate + DELEGATE_FIRST,闭包内部就能直接调用委托对象的方法,无需前缀——MarkupBuilder、Gradle 脚本的秘密都在这里。
💡 关键直觉:把 delegate 想成"闭包的遥控器"——你可以把遥控器换到任何对象上,闭包里的方法调用就按新遥控器执行。Gradle 的
dependencies {}、Groovy 的MarkupBuilder都是这么变出"魔法"的。而this是"遥控器的主机",永远不变。
def calculatePrice = { price, rate -> price * (1 - rate) } def memberPrice = calculatePrice.curry(0.2) // 固定左侧参数 println memberPrice(100) // 80.0
curry 固定左侧参数,rcurry 固定右侧,ncurry 按索引固定。柯里化让参数复用变得自然:同一个折扣函数,会员、普通客户、批量客户各生成一个专用闭包。配合函数组合,可以构建"过滤 → 转换 → 聚合"的流水线。无状态闭包在并发下天然安全,适合并行处理。
Groovy 给 JDK 集合类加了一批方法(统称 GDK):each 遍历、find 找第一个匹配、findAll 全匹配、collect 映射、groupBy 分组。它们把迭代逻辑藏进内部,你只提供"对每个元素做什么"。
def users = [ [name: 'Alice', dept: 'R&D', active: true], [name: 'Bob', dept: 'Ops', active: false], [name: 'Carol', dept: 'R&D', active: true] ] def activeNames = users.findAll { it.active } .collect { it.name.toUpperCase() } println activeNames // [ALICE, CAROL] def byDept = users.groupBy { it.dept }
这段逻辑在 Java 里要写循环、临时变量、中间集合,Groovy 用链式声明式表达完。代价是链式调用会创建多个中间集合,海量数据时性能略逊于单次循环——这时候可以用惰性迭代器或 @CompileStatic 优化。
| 维度 | Java 传统循环 | Groovy 闭包式 |
|---|---|---|
| 状态管理 | 手动索引、边界、自增 | 语言内部封装 |
| 出错点 | off-by-one、空指针 | 基本消除 |
| 关注点 | 怎么迭代 | 对元素做什么 |
| 性能 | 单次循环最优 | 中间对象多,海量数据需优化 |
| 可读性 | 逻辑埋在样板里 | 意图直接可见 |
闭包虽强,但有两类误用要警惕。第一类是"为了闭包而闭包"——简单循环用 each 只是换了个写法,没有带来可读性提升。判断标准:如果闭包让代码更贴近意图,用它;如果只是形式上的替换,不如直接循环。第二类是嵌套闭包过度——三层以上的嵌套会让调用栈难以追踪,delegate 混乱时尤其难查。规范做法是在闭包开头显式声明 def self = delegate 或明确命名委托对象。
⚠️ 常见坑:闭包捕获可变外部变量并在并发下使用,产生数据竞争。闭包不是线程安全自动化的魔法,它只是"方便捕获"的语法。并发场景请用不可变数据,或改用第 7.2 节讲的并发抽象。
一是 DSL/构建器,利用 delegate 机制让代码读起来像业务语言(第 5 章展开)。二是回调与钩子,把"什么时候做什么"封装起来,例如资源管理、事件处理。三是集合管线,把数据清洗、转换、聚合写成一条链。这三个用途覆盖了 Groovy 在生产环境里八成以上的闭包使用场景。
闭包不是银弹。三个场景建议少用:高性能热点路径(闭包对象创建有开销,加 @CompileStatic 会好些但有限);逻辑极其简单、一行 for 就能讲清楚的地方;以及需要严格类型契约的公共 API——闭包参数会让签名变模糊,不如定义明确的行为接口。
三点:一是本质不同,Groovy 闭包是对象,Java Lambda 是函数式接口的实现;二是变量捕获不同,Groovy 闭包可捕获并修改局部变量,Java Lambda 要求 effectively final;三是用途不同,Groovy 闭包通过 delegate 可以做上下文切换(DSL 的核心),Java Lambda 没有这个机制。所以"Groovy 闭包 = Java 的 Lambda + 更多"更接近事实。
在 IDE 里打断点,看闭包实例的 delegate 和 owner 字段值。也可以在闭包开头打印 this、owner、delegate。如果结果不符合预期,多半是解析策略或委托对象设置错了。
it 在闭包逻辑简单时很清爽,但逻辑一复杂就变成"it 到底是谁"。规范做法:闭包体超过三行就显式命名参数,如 { user -> ... }。可读性的优先级高于代码长度。
前面讲的是闭包的基础,这里补充三个生产环境里真正值钱的高级用法。
第一个是记忆化(Memoization)。如果一个闭包计算结果昂贵且输入有限,可以手工实现缓存:用闭包捕获一个 Map,调用前先查缓存。这种模式在规则引擎、解析器里很常见。Groovy 虽然没内置记忆化语法,但闭包"能捕获状态"这个特性让手写缓存只要几行。
第二个是闭包作为构建器上下文。这是 Groovy 最引以为傲的用法:把闭包的 delegate 指向某个上下文对象,闭包内的所有方法调用都转译成上下文对象的方法。MarkupBuilder 就是这么让 html { body { div '内容' } } 变成一棵 XML 树的。这个机制是第 5 章 DSL 的地基,这里先建立直觉:闭包不只是代码块,还是"可切换执行环境"的代码块。
第三个是闭包的递归与委托链。闭包可以调用自身(给闭包变量在定义后引用自己),也能在嵌套时通过 owner 找到外层闭包。这种能力让"层叠配置"成为可能——内层闭包想读外层上下文,owner 链就是天然的查找路径。理解 owner 链,是调试嵌套 DSL 的关键。
很多人担心闭包慢,其实要分情况。闭包对象本身有创建开销,但调用经过调用站点缓存后,热点路径上并不离谱。真正昂贵的场景是:闭包被大量创建且只调用一次(比如在循环里 each 内再创建闭包)、闭包捕获大量外部变量导致对象体积膨胀、闭包作为参数频繁装箱。优化手段按优先级:能用集合方法就别手写循环内闭包,能复用闭包就别反复创建,@CompileStatic 能把部分闭包调用降到接近 Lambda。
def buildFilter(Map criteria, Closure cond) { def result = criteria.findAll(cond) return result } def users = [alice: 30, bob: 25, carol: 41] def adult = buildFilter(users) { k, v -> v >= 30 } println adult // [alice:30, carol:41]
这个例子里,闭包被当作"条件策略"传入通用函数,函数只负责遍历,策略由调用方决定。这就是策略模式在闭包时代的写法——不需要接口、不需要实现类,一个代码块搞定。第 8.1 节会系统讲这类"模式的内化"。
闭包强大,也容易踩到一类隐蔽的坑:闭包内部引用的变量,到底来自哪里?这里有一个典型事故。某团队写了一个配置加载器,闭包里访问一个 settings 变量,预期来自 delegate;但实际闭包是从别的类定义并传入的,settings 在 owner 里找到的却是另一个对象。结果配置读错了值,而且是偶发、难复现——因为 owner 链取决于闭包被定义的位置,而不是被调用的位置。
这类问题的排查思路有三条。第一,明确设置 delegate 并显式指定 resolveStrategy,不要依赖默认值;第二,在闭包开头打印 this、owner、delegate,确认它们各自是谁;第三,对外暴露的闭包 API 要写清楚"闭包内可用哪些变量、它们来自哪一层"。第 5 章做 DSL 时,这套纪律会从"建议"变成"必须"。
Groovy 不是纯函数式语言,它的函数式是"借"来的。正确姿势是:用闭包处理"局部变换"(集合映射、条件筛选、回调注入),但别试图把整个业务写成纯函数链。一旦闭包链超过四五步、中间状态难以观察,可读性会断崖式下降。实践上,我们更推荐"混合风格":外层用面向对象的类和对象组织边界,内层用闭包做精细操作。这既保留了 Groovy 的简洁,又让系统边界清晰可维护。
闭包解决的是"行为抽象",下一节解决"表达精准"——控制流与运算符的增强让你写出既安全又贴业务语义的判断和运算。