
使用闭包提升你的 Apache Groovy 技能
作为一名使用 Apache Groovy 的开发者,理解闭包至关重要。闭包提供了一种简洁而强大的方式来编写清晰、函数式的代码。它们可以将逻辑和数据封装在一个代码块中,允许你将功能作为参数传递给方法,或者动态创建自定义数据结构。这种灵活性简化了复杂的操作并提高了代码可读性,使你成为一名更高效的 Groovy 开发者。(如果你还没有安装 Groovy,请 阅读介绍 以了解这个 系列。)
让我们来看看 Groovy 为了简化 Java 程序员的工作而引入的最重要的概念之一 — Closure
。
不相信我?这是 Venkat Subramaniam 在他 2008 年出版的 “Programming Groovy – Dynamic Productivity for the Java Developer” 一书中说的 (该书已于 2013 年更新,点击此处)
闭包是你最常用的 Groovy 功能之一… GDK 最大的贡献之一是使用接受闭包的方法扩展了 JDK。
当然,Groovy 并没有发明闭包的概念;根据 关于闭包的维基百科文章,
闭包的概念在 20 世纪 60 年代为 λ 演算中表达式的机械求值而开发,并于 1970 年首次作为语言特性在 PAL 编程语言中完全实现,以支持词法作用域的一等函数。
“一等函数” 部分尤其适用于 Groovy 为你带来的功能。在本文中,我将研究你可以使用那些扩展的 JDK “接受闭包的方法” 做哪些事情。在下一篇文章中,你将学习更多关于自己编写这些方法的内容。
GDK 中真正有用的扩展之一是在 Collection
, List
, Map
和其他相关接口中添加了各种接受闭包的方法。新的 Groovy 程序员遇到的第一个这样的方法之一是 each
。这是一个简单的例子
1 def l = [1,2,3,4,5]
2 l.each({ int i ->
3 println "i = $i"
4 })
让我们仔细回顾一下
第一行定义了一个 ArrayList
实例 l
,它有五个元素,其值均为整数。
第二行在 l
上调用 each()
方法。每次调用都会迭代 l
的元素,并调用作为参数传递的 Closure
实例,并将 l
的当前元素作为参数传递给它。Closure
实例是 { … }
结构,从第二行开始,到第四行结束。Groovy 将看起来有点像 C “代码块” — { 和 } 之间的语句 – 视为对 new Closure()
的调用。这是对经常出现的结构模式的一些很好的语法支持。请注意 int i ->
是如何声明 Closure
实例的参数的。
第三行是 Closure
实例的实际可执行主体。在本例中,只是打印出参数 i
。
第四行结束闭包并关闭 each()
调用。
当你运行它时,你会看到
$ groovy Groovy14a.groovy
i = 1
i = 2
i = 3
i = 4
i = 5
熟悉 Java 中的 lambda 的读者应该会认出这种结构。但值得一提的是,Groovy 闭包早于 Java lambda。
这是一个提醒读者方法调用的参数周围的括号是不必要的的好时机。也没有必要定义闭包参数的类型,因为 Groovy 是一种动态语言,会在运行时弄清楚这些东西。
Groovy 闭包也提供了一些额外的优势。其中一个特别之处是 Groovy 闭包可以访问闭包主体外部范围内的任何内容。但 Java lambda 只能访问 “effectively final” 的东西。这是一个使用它来对数字列表求和的例子
1 def l = [1,2,3,4,5]
2 int lsum = 0
3 l.each { i ->
4 lsum += i
5 }
6 println "lsum = $lsum"
运行时,看起来像
$ groovy Groovy14b.groovy
lsum = 15
当然,这种方法通常不受欢迎,因为它需要变量 lsum
的可变性。这被一些人视为导致可维护性问题。当不必要地使用可变变量时,就会发生这种情况。它最终可能会在以后的维护周期中被篡改。
为了避免需要可变的 lsum
,你可以使用 inject()
方法而不是 each()
1 def l = [1,2,3,4,5]
2 final int lsum = l.inject(0) { ps, i ->
3 ps + i
4 }
5 println "lsum = $lsum"
对于那些遵循 Groovy GDK 文档 并想知道为什么他们在 List
方法列表中找不到 inject()
的人,inject()
是在 Iterator
接口中定义的。
让我们深入研究一下第二行
- 你声明
lsum
为 final,这意味着一旦赋值,其值就不能更改。 inject()
方法接受两个参数:一个起始值,这里是 0,以及一个Closure
实例,并迭代l
的元素。- 提供给
inject()
的Closure
实例接受两个参数 — 第一个参数是ps
,意思是 “partial sum”(部分和),是到目前为止的结果。第二个参数i
是当前正在检查的元素的值。它返回处理完当前元素后的新结果,这里是将两个参数相加。
当你运行这个脚本时,你会看到
$ groovy Groovy14c.groovy
lsum = 15
对于那些熟悉 map — reduce 范式的人来说,Groovy 的 inject()
实现了 reduce 操作。这带你到了 map 操作,在 Groovy 中称为 collect()
。你可以使用 collect()
将列表 l
的元素映射到它们的平方
1 def l = [1,2,3,4,5]
2 def lsq = l.collect { i ->
3 i * i
4 }
5 println "lsq = $lsq"
在这里你可以看到 collect()
接受一个参数,即正在检查的列表元素,并返回其转换后的或映射后的值。运行这个
$ groovy Groovy14d.groovy
lsq = [1, 4, 9, 16, 25]
另一个额外的很酷的方法,提供过滤能力的是 findAll()
方法。使用 findAll()
过滤掉列表中的奇数
1 def l = [1,2,3,4,5]
2 def lo = l.findAll { i ->
3 i % 2 != 0
4 }
5 println "lo = $lo"
在这里你可以看到 findAll()
接受一个参数,即正在检查的列表元素。它返回一个列表,其元素满足闭包主体中提供的测试表达式。运行这个
$ groovy Groovy14e.groovy
lo = [1, 3, 5]
方法 findAll()
查找所有满足提供的闭包中的测试条件的元素。你可能不会惊讶地发现还有一个 find()
方法,它找到第一个元素然后停止。
我不打算详细介绍所有接受闭包作为参数的优秀的 List
方法,但这里有两个我认为有用的方法
split()
看起来类似于findAll()
,但返回一个包含两个子列表的列表。第一个子列表包含在测试表达式中产生 true 值的元素。第二个子列表是在测试表达式中产生 false 值的元素。takeWhile()
也看起来类似于findAll()
,但返回一个列表,其中包含在测试表达式中产生 true 值的直到找到第一个 false 值的元素。
转到 Iterator
接口,所有接受闭包的方法中最有用的方法之一是 collectEntries()
。这可以与 List
接口的 transpose()
方法结合使用,从两个元素子列表的列表中构建 Map
,如下所示
1 def keys = [1,2,3,4,5]
2 def values = ["i","ii","iii","iv","v"]
3 def map = [keys,values].transpose().collectEntries { k, v ->
4 [(k): v]
5 }
6 println "map = $map"
第一行到第二行定义了两个列表,第一个列表包含 map 的预期键。第二个列表应用要分配给这些键的值。
第三行创建了键和值的两个子列表的列表,并在其上调用 transpose()
以配对键值对。这将创建一个包含五个两元素子列表的列表 [1,”i”], [2,”ii”’] 等等。它还调用 collectEntries()
,并将一个闭包传递给它,该闭包将每个两元素子列表转换为 MapEntry
实例。
第四行返回一个包含一个 MapEntry
的 Map
实例。回想一下,在 Groovy 中,使用语法 [a: b]
声明 Map
实例会将 a
视为 String
。但是 [(a): b]
会导致 a
被评估。
当你运行它时,你会看到
$ groovy Groovy14f.groovy
map = [1:i, 2:ii, 3:iii, 4:iv, 5:v]
当然,Map 接口也定义了一堆有趣的以 Closures 作为参数的方法
- each() 迭代 Map 中的每个 MapEntry,调用闭包并将 MapEntry 传递给单参数闭包或键和值传递给双参数闭包。
- inject() 类似地将 reduce 方法应用于 map。
- findAll() 迭代并过滤 Map。
- findResults() 类似地迭代 Map,过滤掉 null 结果。
- groupBy() 迭代 Map,根据计算出的键值将 MapEntry 组组合成子 map。例如,具有整数键的 map 可以拆分为两个子 map,一个子 map 的键为 “even”,包含所有具有偶数键的 MapEntry 实例。其余的进入另一个键为 “odd” 的子 map。
这是一个很好的时机,建议转到 Groovy GDK 文档 中方便地放在一起的 Collection
, List
, Map
和 Interface
的参考描述。你也应该阅读 关于 Closure 的 Groovy 文档。
结论
Groovy 闭包早于 Java lambda,并提供了一些有趣的优势。它们不限于引用包含范围内的 effectively final 变量。它们更容易学习(至少从我的角度来看),因为它们不依赖于涉及流、函数等的新而复杂的一组类。相反,它们的实用性附加到众所周知的 JDK 类,包括 List
和 Map
。
而且,像往常一样,Groovy 提供了一些强大的语法糖来使基于闭包的代码简洁易读,从而方便日后的维护。敬请期待更多内容!
3 条评论
评论已关闭。