two red and blue zippers

使用闭包提升你的 Apache Groovy 技能

理解 Apache Groovy 中的闭包至关重要。闭包提供了一种简洁而强大的方式来编写…
首页 » 博客 » 使用闭包提升你的 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 实例。

第四行返回一个包含一个 MapEntryMap 实例。回想一下,在 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, MapInterface 的参考描述。你也应该阅读 关于 Closure 的 Groovy 文档

结论

Groovy 闭包早于 Java lambda,并提供了一些有趣的优势。它们不限于引用包含范围内的 effectively final 变量。它们更容易学习(至少从我的角度来看),因为它们不依赖于涉及流、函数等的新而复杂的一组类。相反,它们的实用性附加到众所周知的 JDK 类,包括 ListMap

而且,像往常一样,Groovy 提供了一些强大的语法糖来使基于闭包的代码简洁易读,从而方便日后的维护。敬请期待更多内容!

作者

如果你喜欢这篇文章,你可能也会喜欢这些