使用 Groovy™ 探索 Gatherers4J
发布时间:2025-05-06 08:30PM
让我们探索如何将 Groovy 与 Gatherers4J 库和 JDK 24 gatherers API 结合使用。我们还将为 JDK 8/11+ 用户提供迭代器变体! |
最近 JDK 版本的一个有趣特性是 Gatherers。JDK 24 包含了一系列内置的 gatherers,但 gatherers API 的主要目标是允许开发自定义中间操作,而不是提供大量内置 gatherers。
我们在早期的博客文章中探讨了如何为 chop、collate 和其他内置 Groovy 功能编写自己的 gatherer 等价物。
其他人也一直在研究有用的 gatherers,并且库也开始出现。Gatherers4J 就是其中一个库。我们将使用 0.11.0、Groovy 5 快照(预发布)和 JDK 24。
现在,让我们看看 Gatherers4J 库中的许多其他 gatherers(以及它们在 Groovy 中的迭代器等价物)。
使用 Gatherers4J 重新审视 Collate
在早前的博客文章中,我们展示了如何为 Groovy 在集合和迭代器上的 collate
扩展方法提供流等价物。它的一些功能受到内置的 windowFixed
和 windowSliding
gatherers 的支持,我们展示了如何编写一些自定义 gatherers,即 windowSlidingByStep
和 windowFixedTruncating
,以处理其余功能。
让我们看看转而使用 Gatherers4J 中的 window
gatherer
assert (1..5).stream().gather(Gatherers4j.window(3, 1, true)).toList() ==
[[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5], [5]]
assert (1..8).stream().gather(Gatherers4j.window(3, 2, true)).toList() ==
[[1, 2, 3], [3, 4, 5], [5, 6, 7], [7, 8]]
assert (1..8).stream().gather(Gatherers4j.window(3, 2, false)).toList() ==
[[1, 2, 3], [3, 4, 5], [5, 6, 7]]
assert (1..8).stream().gather(Gatherers4j.window(3, 4, false)).toList() ==
[[1, 2, 3], [5, 6, 7]]
assert (1..8).stream().gather(Gatherers4j.window(3, 3, true)).toList() ==
[[1, 2, 3], [4, 5, 6], [7, 8]]
为了比较,以下是使用 Groovy 在集合上的 collate
扩展方法显示的输出
assert (1..5).collate(3, 1) == [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5], [5]]
assert (1..8).collate(3, 2) == [[1, 2, 3], [3, 4, 5], [5, 6, 7], [7, 8]]
assert (1..8).collate(3, 2, false) == [[1, 2, 3], [3, 4, 5], [5, 6, 7]]
assert (1..8).collate(3, 4, false) == [[1, 2, 3], [5, 6, 7]]
assert (1..8).collate(3, 3) == [[1, 2, 3], [4, 5, 6], [7, 8]]
它们并非完全等价。Groovy 版本对集合进行急切操作,而 gatherer 版本是基于流的。我们可以通过 Groovy 的 collate
方法的迭代器变体来展示更“苹果与苹果”的比较。让我们看一些无限流示例。
gatherer 版本
assert Stream.iterate(0, n -> n + 1)
.gather(Gatherers4j.window(3, 3, true))
.limit(3)
.toList() == [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
注意
|
因为我们不担心余数或不同的步长,所以这里可以直接使用 JDK 24 内置的 Gatherers.windowFixed(3) gatherer。 |
现在,让我们看看 Groovy 的迭代器等价物
assert Iterators.iterate(0, n -> n + 1)
.collate(3)
.take(3)
.toList() == [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
当我们说等价时,我们并不是暗示迭代器提供与流相同的能力或灵活性,但在许多简单场景中,它们将达到相同的结果并且可能更高效。
探索 Gatherers4J 的其他功能
现在让我们探索 Gatherers4J 中的一些其他 gatherers。为了进行“某种程度上”的“苹果对苹果”比较,我们将与等效的 Groovy 迭代器功能进行比较,除非另有说明。在某些情况下,Groovy 不提供基于迭代器的等效功能,因此在这些情况下我们将查看基于集合的解决方案。
请记住,根据您的场景,使用这些示例的集合变体可能更简单且同样适用。流式解决方案在处理大型流时确实表现出色,但我们需要在此类博客文章中保持示例简单。
对于 Gatherers4J 示例,我们将使用 浅绿色 背景;对于标准 Groovy 迭代器功能,我们将使用 浅蓝色 背景。对于某些示例,我们还将介绍使用 Groovy-stream 库的情况,在这种情况下,我们将使用 浅橙色 背景。
Gatherers4J 拥有 50 多个 gatherers,分为五种不同的类别
-
序列操作 - 重新排序、组合或操纵元素的序列。
-
过滤和选择 - 根据某些标准选择或删除元素。
-
分组和窗口化 - 将元素收集到组或窗口中。
-
数学运算 - 对流执行计算。
-
验证和约束 - 对流强制执行条件。
我们将探索其中大约一半,并主观地选择可能更常见的场景,但如果您正在使用流,整个库都值得一试。值得记住的是,JDK 具有 Gatherers4J 不试图复制的内置功能。从这个意义上说,Gatherers4J 确实有一些不常用到的 gatherers,但许多仍然非常有用。
我们进行这些比较的目的不是为了将 gatherer 解决方案或迭代器解决方案视为竞争对手。流和迭代器各有其优点。如果使用其中任何一个,最好了解另一个可能是什么样子以及它可能提供什么。
在开始之前,让我们创建一些将在后续示例中使用的变量
var abc = 'A'..'C'
var abcde = 'A'..'E'
var nums = 1..3
crossWith, combine
让我们看看如何创建两个源之间的所有组合对,即 笛卡尔积。
Gatherers4J 提供了 crossWith
gatherer
assert abc.stream()
.gather(Gatherers4j.crossWith(nums.stream()))
.map(pair -> pair.first + pair.second)
.toList() == ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3']
对于集合,Groovy 提供了 combinations
或 eachCombination
,但对于迭代器,您可以执行以下操作
assert Iterators.combine(letter: abc.iterator(), number: nums.iterator())
.collecting(map -> map.letter + map.number)
.toList() == ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3']
Groovy 的 collect
方法家族对应于 Java 流中的 map
方法。Groovy 在命名该方法时遵循了 Smalltalk 的命名约定。Groovy 提供了急切的 collect
和惰性的 collecting
变体,因此我们可以在这里使用 collect
而无需 toList()
。
foldIndexed, inject+withIndex
折叠(又称 注入 和 归约)操作将项目流折叠成单个值。让我们探索一个折叠的变体,它还提供项目的索引。
Gatherers4J 提供了 foldIndexed
gatherer
assert abc.stream()
.gather(Gatherers4j.foldIndexed(
() -> '', // initialValue
(index, carry, next) -> carry + next + index
))
.findFirst().get() == 'A0B1C2'
Groovy 使用 inject
作为 fold 的名称,但没有提供带索引值的特殊变体。相反,您可以将普通的 inject 与 withIndex
或 indexed
结合使用
assert abc.iterator().withIndex().inject('') { carry, next ->
carry + next.first + next.last
} == 'A0B1C2'
interleaveWith, interleave
interleaveWith
gatherer 交错两个流中的元素
assert abc.stream()
.gather(Gatherers4j.interleaveWith(nums.stream()))
.toList() == ['A', 1, 'B', 2, 'C', 3]
Groovy 支持 interleave
assert abc.iterator()
.interleave(nums.iterator())
.toList() == ['A', 1, 'B', 2, 'C', 3]
mapIndexed, collect+withIndex, mapWithIndex
映射(也称为 转换 或 收集)操作将流中的元素转换为新值的流。
Gatherers4J 中的 mapIndexed
gatherer 提供了对每个元素及其索引的访问
assert abc.stream()
.gather(Gatherers4j.mapIndexed (
(i, s) -> s + i
))
.toList() == ['A0', 'B1', 'C2']
在 Groovy 中,您通常会使用 withIndex
和 collect
来实现此功能
assert abc.iterator()
.withIndex()
.collect { s, i -> s + i } == ['A0', 'B1', 'C2']
或者我们可以使用这里所示的 collecting
变体
assert abc.iterator()
.withIndex()
.collecting { s, i -> s + i }
.toList() == ['A0', 'B1', 'C2']
Groovy-stream 为此场景提供了一个 mapWithIndex
方法
assert Stream.from(abc)
.mapWithIndex { s, i -> s + i }
.toList() == ['A0', 'B1', 'C2']
orderByFrequency, countBy
Gatherers4J 中的 orderByFrequency
gatherer 计算流中的元素,然后在流完成后,返回流中的唯一值(及其频率计数),并按频率计数顺序(升序或降序)排列。鉴于此行为,它可以作为收集器实现。我们将介绍使用内置收集器,然后为了完整性,展示 Gatherers4J 中的升序和降序变体
var letters = ['A', 'A', 'A', 'B', 'B', 'B', 'B', 'C', 'C']
assert letters.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.toString() == '[A:3, B:4, C:2]'
assert letters.stream()
.gather(Gatherers4j.orderByFrequency(Frequency.Ascending))
.map(withCount -> [withCount.value, withCount.count])
.toList()
.collectEntries()
.toString() == '[C:2, A:3, B:4]'
assert letters.stream()
.gather(Gatherers4j.orderByFrequency(Frequency.Descending))
.map(withCount -> [withCount.value, withCount.count])
.toList()
.collectEntries()
.toString() == '[B:4, A:3, C:2]'
Groovy 在集合上的 countBy
方法执行类似的操作,但默认情况下,它按首次出现的顺序返回唯一项。由于它有点像终端操作符,我们这里只使用 Groovy 集合方法。升序和降序行为可以通过排序实现
assert letters.countBy().toString() == '[A:3, B:4, C:2]'
assert letters.countBy()
.sort{ e -> e.value }
.toString() == '[C:2, A:3, B:4]'
assert letters
.countBy()
.sort{ e -> -e.value }
.toString() == '[B:4, A:3, C:2]'
peekIndexed, tapEvery+withIndex, tapWithIndex
Gatherers4J 中的 peekIndexed
gatherer 类似于 JDK Streams 的 peek
中间操作,但也提供对索引值的访问
assert abc.stream()
.gather(Gatherers4j.peekIndexed(
(index, element) -> println "Element $element at index $index"
))
.toList() == abc
Groovy 的 eachWithIndex
提供了类似的功能,但会耗尽迭代器。相反,您可以将 withIndex
与 tapEvery
结合使用
assert abc.iterator().withIndex().tapEvery { tuple ->
println "Element $tuple.first at index $tuple.last"
}*.first == abc
Groovy-stream 提供了一个 tapWithIndex
方法
assert Stream.from(abc)
.tapWithIndex { s, i -> println "Element $s at index $i" }
.toList() == abc
以上所有操作都会产生以下输出
Element A at index 0 Element B at index 1 Element C at index 2
repeat
Gatherers4J 有一个 repeat
gatherer,允许将元素源重复给定次数
assert abc.stream()
.gather(Gatherers4j.repeat(3))
.toList() == ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C']
Groovy 为集合提供了 multiply
运算符,但对于迭代器也有一个 repeat
方法
assert abc * 3 == ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C']
assert abc.iterator()
.repeat(3)
.toList() == ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C']
Groovy-streams 也有一个 repeat
方法
assert Stream.from(abc)
.repeat(3)
.toList() == ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C']
repeatInfinitely
Gatherers4J 有一个 repeatInfinitely
gatherer,允许元素源无限循环重复
assert abc.stream()
.gather(Gatherers4j.repeatInfinitely())
.limit(5)
.toList() == ['A', 'B', 'C', 'A', 'B']
Groovy 为此场景提供了一个 repeat
方法
assert abc.iterator()
.repeat()
.take(5)
.toList() == ['A', 'B', 'C', 'A', 'B']
Groovy-stream 为此提供了一个 repeat
方法
assert Stream.from(abc)
.repeat()
.take(5)
.toList() == ['A', 'B', 'C', 'A', 'B']
reverse
Gatherers4J 中的 reverse
gatherer 在接收到所有元素后,以相反的顺序返回流中的元素
assert abc.stream()
.gather(Gatherers4j.reverse())
.toList() == 'C'..'A'
由于在输出元素之前检查了整个流,因此这里使用流没有显著优势,并且 reverse
不适合用于处理无限流。
这同样适用于 Groovy 的迭代器实现,但 Groovy,像 Gatherers4J 一样,无论如何都提供了一个 reverse
扩展方法
assert abc.iterator()
.reverse()
.toList() == 'C'..'A'
rotate
Gatherers4J 中的 rotate
gatherer 在接收到所有元素后,以旋转的位置返回流中的元素。同样,由于处理了整个流,与使用集合相比,这里没有显著优势。我们可以向任一方向旋转
var abcde = ['A', 'B', 'C', 'D', 'E']
var shift = 2
assert abcde.stream()
.gather(Gatherers4j.rotate(Rotate.Left, shift))
.toList() == ['C', 'D', 'E', 'A', 'B']
assert abcde.stream()
.gather(Gatherers4j.rotate(Rotate.Right, shift))
.toList() == ['D', 'E', 'A', 'B', 'C']
Groovy 不提供任何基于迭代器的等效方法。对于集合,如果可变方法可以接受,用户可以利用 JDK 库中的 Collections.rotate
var temp = abcde.clone() // unless mutating original is okay
Collections.rotate(temp, -shift) // -ve for left
assert temp == ['C', 'D', 'E', 'A', 'B']
temp = abcde.clone()
Collections.rotate(temp, shift) // +ve for right
assert temp == ['D', 'E', 'A', 'B', 'C']
或者,Groovy 的索引允许对集合进行非常灵活的切片,因此可以通过这种索引操作来实现旋转
assert abcde[shift..-1] + abcde[0..<shift] == ['C', 'D', 'E', 'A', 'B'] // left
assert abcde[shift<..-1] + abcde[0..shift] == ['D', 'E', 'A', 'B', 'C'] // right
基于迭代器的 rotate 扩展方法可能是 Groovy 未来可能的一个特性。对于左移,似乎可以不存储整个列表,就像当前的 Gatherers4J 实现那样,而只存储“位移”距离的元素数量。对于右移,您需要流大小减去“位移”距离,并且您无法提前知道大小。
scanIndexed, injectAll+withIndex
Gatherers4J 提供了 scanIndexed
gatherer。它类似于 JDK 内置的 scan
gatherer,但也提供了对索引的访问
assert abc.stream()
.gather(
Gatherers4j.scanIndexed(
() -> '',
(index, carry, next) -> carry + next + index
)
)
.toList() == ['A0', 'A0B1', 'A0B1C2']
对于 Groovy,可以使用 injectAll
方法与 withIndex
结合使用
assert abc.iterator()
.withIndex()
.injectAll('') { carry, next ->
carry + next.first + next.last
}
.toList() == ['A0', 'A0B1', 'A0B1C2']
shuffle
Gatherers4J 提供了 shuffle
gatherer
int seed = 42
assert Stream.of(*'A'..'G')
.gather(Gatherers4j.shuffle(new Random(seed)))
.toList() == [ 'B', 'D', 'F', 'A', 'E', 'G', 'C' ]
这是另一个会消耗整个流并将其保存在内存中,然后才产生值的 gatherer。与使用集合相比,没有显著优势。Groovy 只提供基于集合的此功能,使用 shuffled
扩展方法
assert ('A'..'G').shuffled(new Random(seed)) == ['C', 'G', 'E', 'A', 'F', 'D', 'B']
withIndex
Gatherers4J 和 Groovy 都提供了 withIndex
。这是 gatherer 版本
assert abc.stream()
.gather(Gatherers4j.withIndex())
.map(withIndex -> "$withIndex.value$withIndex.index")
.toList() == ['A0', 'B1', 'C2']
这是迭代器版本
assert abc.iterator().withIndex().collect { s, i -> s + i } == ['A0', 'B1', 'C2']
zipWith, zip
Gatherers4J 提供了一个 zipWith
gatherer
assert abc.stream()
.gather(Gatherers4j.zipWith(nums.stream()))
.map(pair -> "$pair.first$pair.second")
.toList() == ['A1', 'B2', 'C3']
Groovy 提供 zip
assert abc.iterator()
.zip(nums.iterator())
.collect { s, n -> s + n } == ['A1', 'B2', 'C3']
Groovy-stream 提供了相同的功能
assert Stream.from(abc)
.zip(nums) { s, i -> s + i }
.toList() == ['A1', 'B2', 'C3']
distinctBy, toUnique
Gatherers4J 有一个 distinctBy
gatherer,它使用谓词来确定相等性,从而查找唯一元素
assert Stream.of('A', 'BB', 'CC', 'D')
.gather(Gatherers4j.distinctBy(String::size))
.toList() == ['A', 'BB']
Groovy 为此提供了 toUnique
assert ['A', 'BB', 'CC', 'D'].iterator()
.toUnique(String::size)
.toList() == ['A', 'BB']
dropEveryNth/takeEveryNth
Gatherers4J 有特殊的 gatherers,用于获取或丢弃每第 N 个元素
// drop every 3rd
assert ('A'..'G').stream()
.gather(Gatherers4j.dropEveryNth(3))
.toList() == ['B', 'C', 'E', 'F']
// take every 3rd
assert ('A'..'G').stream()
.gather(Gatherers4j.takeEveryNth(3))
.toList() == ['A', 'D', 'G']
Groovy 没有用于获取/丢弃第 n 个元素的特定方法,但您可以使用 findAllLazy
和 withIndex
,或者 tapEvery
// drop every 3rd
assert ('A'..'G').iterator().withIndex()
.findAll { next, i -> i % 3 }
*.first == ['B', 'C', 'E', 'F']
// take every 3rd
assert ('A'..'G').iterator().withIndex()
.findAll { next, i -> i % 3 == 0 }
*.first == ['A', 'D', 'G']
// also take every 3rd
var result = []
('A'..'G').iterator().tapEvery(3) { result << it }.toList()
assert result == ['A', 'D', 'G']
Groovy-stream 也没有特定的方法,但您可以使用 filterWithIndex
或 tapEvery
实现类似的功能
// drop every 3rd
assert Stream.from('A'..'G')
.filterWithIndex { next, idx -> idx % 3 }
.toList() == ['B', 'C', 'E', 'F']
// take every 3rd
assert Stream.from('A'..'G')
.filterWithIndex { next, idx -> idx % 3 == 0 }
.toList() == ['A', 'D', 'G']
// also take every 3rd (starting from 3rd)
var result = []
Stream.from('A'..'G').tapEvery(3) { result << it }.toList()
assert result == ['C', 'F']
result = []
dropLast, dropRight
dropLast
gatherer 从流中删除最后 n 个元素
assert abcde.stream()
.gather(Gatherers4j.dropLast(2))
.toList() == abc
Groovy 为此提供了 dropRight
assert abcde.iterator().dropRight(2).toList() == abc
filterIndexed
filterWithIndex
gatherer 允许过滤元素并访问索引
assert abcde.stream()
.gather(Gatherers4j.filterIndexed{ n, s -> n % 2 == 0 })
.toList() == ['A', 'C', 'E']
assert abcde.stream()
.gather(Gatherers4j.filterIndexed{ n, s -> n < 2 || s == 'E' })
.toList() == ['A', 'B', 'E']
Groovy 没有一体化的等效功能,但您可以使用 withIndex
加上 findAllLazy
assert abcde.iterator().withIndex()
.findAll { s, n -> n % 2 == 0 }*.first == ['A', 'C', 'E']
assert abcde.iterator().withIndex()
.findAll { s, n -> n < 2 || s == 'E' }*.first == ['A', 'B', 'E']
Groovy-stream 提供了一个 filterWithIndex
方法
assert Stream.from(abcde)
.filterWithIndex{ s, n -> n % 2 == 0 }
.toList() == ['A', 'C', 'E']
assert Stream.from(abcde)
.filterWithIndex{ s, n -> n < 2 || s == 'E' }
.toList() == ['A', 'B', 'E']
filterInstanceOf
filterInstanceOf
gatherer 将 filter
与 instanceof
结合起来
var mixed = [(byte)1, (short)2, 3, (long)4, 5.0, 6.0d, '7', '42']
assert mixed.stream()
.gather(Gatherers4j.filterInstanceOf(Integer))
.toList() == [3]
assert mixed.stream()
.gather(Gatherers4j.filterInstanceOf(Number))
.toList() == [1, 2, 3, 4, 5.0, 6.0]
assert mixed.stream()
.gather(Gatherers4j.filterInstanceOf(Integer, Short))
.toList() == [2, 3]
Groovy 没有完全等效的功能,但它有一个急切的 grep
,可以让你做类似的事情,以及其他功能,如匹配正则表达式模式。你也可以根据需要结合使用 findAllLazy
和 getClass()
或 instanceof
var mixed = [(byte)1, (short)2, 3, (long)4, 5.0, 6.0d, '7', '42']
assert mixed.iterator().grep(Integer) == [3]
assert mixed.iterator().grep(Number) == [1, 2, 3, 4, 5.0, 6.0]
assert mixed.iterator().grep(~/\d/).toString() == '[1, 2, 3, 4, 7]'
assert mixed.iterator()
.findAllLazy{ it.getClass() in [Integer, Short] }
.toList() == [2, 3]
takeLast/takeRight
takeLast
gatherer 返回流中的最后 n 个元素
assert abcde.stream()
.gather(Gatherers4j.takeLast(3))
.toList() == ['C', 'D', 'E']
它在发出元素之前读取整个流。
Groovy 没有迭代器等价物,但有一个 takeRight
集合方法
assert abcde.takeRight(3) == 'C'..'E'
takeUntil, takeWhile, until
takeUntil
gatherer 提取元素直到某个条件满足,包括满足条件的元素
assert abcde.stream()
.gather(Gatherers4j.takeUntil{ it == 'C' })
.toList() == abc
takeWhile
扩展方法在满足某个条件时提取元素(因此需要相反的条件)
assert abcde.iterator()
.takeWhile { it != 'D' }
.toList() == abc
Groovy-stream 的 until
方法的条件与 gatherers4j 类似,但不包括触发条件的元素
assert Stream.from(abcde)
.until { it == 'D' }
.toList() == abc
uniquelyOccurring
uniquelyOccurring
gatherer 在返回仅出现一次的元素之前读取整个流
assert Stream.of('A', 'B', 'C', 'A')
.gather(Gatherers4j.uniquelyOccurring())
.toList() == ['B', 'C']
Groovy 没有等效的方法,但可以使用 countBy
实现类似的功能。Groovy 的 countBy
方法是急切的(可以看作是终端)操作符
assert ['A', 'B', 'C', 'A'].iterator()
.countBy()
.findAll{ it.value == 1 }*.key == ['B', 'C']
更多信息
结论
我们已经研究了 Gatherers4j 库的一些功能,以及如何使用迭代器实现类似的功能。Gatherers4j 与 Groovy 配合良好,提供了 JDK 中没有的额外流功能。如果您经常使用流,请探索 Gatherers4j,它提供了可能满足您需求的预构建 gatherers。