Groovy 和顺序集合 (JEP-431)

作者: Paul King
发布时间: 2023-04-29 09:00AM


JDK21 中即将推出的一项激动人心的功能是 顺序集合,它为具有定义的遭遇顺序的集合提供了改进的处理。有关新功能的更多详细信息,请参见后面的 其他参考资料 部分。

由于 Groovy 旨在与 JDK 库紧密协作,因此一旦 JDK21 发布,Groovy 将“免费”获得新功能。但是,Groovy 有自己解决 JEP-431 所解决的一些问题的方案,因此这篇文章将介绍现有功能(您可以将其与较旧的 JDK 一起使用)以及一旦 JDK21 普遍可用且您升级到该 JDK 版本后可以使用的新功能。

这篇文章中的示例使用 JDK21ea Build 20(2023/4/27)和 Groovy 4.0.11。虽然 EA 版本附带了许多免责声明,警告在正式发布之前可能会发生功能更改或删除,但我们预计这些示例适用于后续版本。如果出现任何变化,我们会发布更新。

顺序集合摘要

顺序集合添加了三个新的接口:SequencedSetSequencedCollectionSequencedMap。每个接口都添加了一些新方法,我们将在后面的示例中遇到。在本帖子的其余部分,我们将通过查看处理集合时可能遇到的各种场景来解释新的顺序集合功能。

访问第一个和最后一个元素

JEP-431 之前的 JDK

对于各种集合类型,访问第一个和最后一个元素这样简单的事情并不一致,也不像预期的那样容易(因此有了 JEP-431)。以下是一些您在此场景中使用的 JDK API 调用的示例

集合类型 第一个元素 最后一个元素

列表

list.get(0)

list.get(list.size()-1)

双端队列

deque.getFirst()

deque.getLast()

集合

set.iterator().next()
set.stream().findFirst().get()

需要遍历集合

排序集

set.first()

set.last()

JEP-431 之后的 JDK

JEP-431 之后,这有了很大的改进

集合类型 第一个元素 最后一个元素

ListDequeSet

collection.getFirst()

collection.getLast()

JEP-431 之前的 Groovy

Groovy 为数组和任何 Iterable 提供扩展方法 first()last(),在有意义的情况下提供各种优化版本。下标运算符适用于任何具有 getAt 方法的类。对于集合、数组和许多其他类,都有内置的实现。

聚合类型 第一个元素 最后一个元素

ListDequeSet、数组

aggregate[0]
aggregate.first()

aggregate[-1]
aggregate.last()

Groovy 还提供了扩展方法,可以在这里使用。您可以使用 list.take(1) 获取一个仅包含原始列表第一个元素的列表,并使用 takeRight(1) 获取一个仅包含最后一个元素的列表。这些方法也适用于所有不同的聚合类型。

JEP-431 之后的 Groovy

尚未为 JEP-431 添加特殊支持。因此 Groovy 功能将是现有功能加上新的 JDK 功能。

聚合类型 第一个元素 最后一个元素

数组

array[0]
array.first()

array[-1]
array.last()

ListDequeSet

collection[0]
collection.first()
collection.getFirst()
collection.first

collection[-1]
collection.last()
collection.getLast()
collection.last

目前,Groovy 的方法在数组(以及可能的其他类)中也提供了一致性。对于这种情况,人们不应急于使用 JEP-431 功能,但有一个重要的注意事项。随着时间的推移,可能会出现其他集合类型,这些类型可能为 getFirst()getLast() 提供更有效的实现,在这种情况下,使用这些方法将是有益的。

未来的 Groovy 版本可能会提供专门的顺序集合支持。first()last() 扩展方法可能会根据 getFirst()getLast() 来实现。我们也可能会将顺序集合方法扩展到数组(以及其他类),但目前这不是优先事项。

示例

List list = [1, 2, 3]
assert list.get(0) == 1
assert list[0] == 1
assert list.first() == 1
assert list.getFirst() == 1                                // NEW
assert list.first == 1                                     // NEW

assert list.get(list.size() - 1) == 3
assert list[-1] == 3
assert list.last() == 3
assert list.getLast() == 3                                 // NEW
assert list.last == 3                                      // NEW

LinkedList deque = [1, 2, 3]
assert deque[0] == 1
assert deque.first() == 1
assert deque.getFirst() == 1                               // NEW
assert deque.first == 1                                    // NEW

assert deque[-1] == 3
assert deque.last() == 3
assert deque.getLast() == 3                                // NEW
assert deque.last == 3                                     // NEW

LinkedHashSet set = [1, 2, 3]
assert set.iterator().next() == 1
assert set[0] == 1
assert set.first() == 1
assert set.getFirst() == 1                                 // NEW
assert set.first == 1                                      // NEW

assert set[-1] == 3
assert list.last() == 3
assert set.getLast() == 3                                  // NEW
assert set.last == 3                                       // NEW

TreeSet sortedSet = [2, 4, 1, 3]
assert sortedSet[0] == 1
assert sortedSet.first() == 1
assert sortedSet.getFirst() == 1                           // NEW
assert sortedSet.first == 1                                // NEW

assert sortedSet[-1] == 4
assert sortedSet.last() == 4
assert sortedSet.getLast() == 4                            // NEW
assert sortedSet.last == 4                                 // NEW

Integer[] array = [1, 2, 3]
assert array[0] == 1
assert array.first() == 1
assert array[-1] == 3
assert array.last() == 3

删除第一个或最后一个元素

如果您需要修改集合,删除第一个或最后一个元素,Groovy 不会为所有聚合类型提供一致的扩展方法。您可以使用 JDK remove(0) 方法从 List 中删除列表的第一个元素(Groovy 还提供了一个不错的 removeAt(0) 别名)。Groovy 还为列表提供了 removeLast()。鉴于此,SequencedCollection 中的 removeFirst()removeLast() 方法是一个很好的补充。

如果您想创建一个与原始集合相同的新聚合,但删除第一个(或最后一个)元素,Groovy 提供了 tail()drop(1)(或 init()dropRight(1))。

在开头/结尾添加元素

如果您需要修改集合,在开头或结尾添加元素,Groovy 不会为所有聚合类型提供一致的扩展方法。您通常会使用 add(element)add(0, element) 来操作列表。因此,SequencedCollection 中的 addFirst()addLast() 方法是一个很好的补充。Groovy 确实提供了 leftShift 运算符(<<)作为另一种将元素追加到列表末尾的方法。

使用反转的集合

JEP-431 解决的另一个领域是改进与集合反向顺序进行交互的一致性。Groovy 已经为这种情况提供了一些增强功能,包括 reversereverseEachasReversed 扩展方法。但是,功能并不通用,有时会让人措手不及。reverse 方法不适用于映射和集合。您需要使用例如集合的迭代器。此外,标准 reverse 会产生一个新的集合(或数组),并且有一个可选的布尔参数,该参数使该方法成为一个修改操作——就地反转自身。这与 JEP-431 中的 reversed()asReversed() 形成对比,后者返回一个视图。此外,reverseEachasReversed 仅适用于 NavigableSet 实例。

因此,总而言之,JEP-431 提供的此功能非常受欢迎。

集合类型 JEP-431 之前 JEP-431 之后 Groovy

列表

使用 list.listIterator(list.size()).previous()

list.reversed()

list.reverseEach
list.reverse()
list.asReversed()

双端队列

使用 deque.descendingIterator()

deque.reversed()

deque.reverseEach
deque.reverse()
deque.asReversed()

可导航集合

使用 set.descendingSet()

set.reversed()

set.reverseEach
set.asReversed()

Set(其他)

N/A

set.reversed()

set.iterator().reverse()

示例

var result = []
list.reverseEach { result << it }
assert result == [3, 2, 1]
assert list.asReversed() == [3, 2, 1]
assert list.reverse() == [3, 2, 1]
assert list.reversed() == [3, 2, 1]                        // NEW

result = []
deque.reverseEach { result << it }
assert result == [3, 2, 1]
assert deque.asReversed() == [3, 2, 1]
assert deque.reverse() == [3, 2, 1]
assert deque.reversed() == [3, 2, 1]                       // NEW

result = []
assert set.iterator().reverse().toList() == [3, 2, 1]
assert set.reversed() == [3, 2, 1] as Set                  // NEW

result = []
sortedSet.reverseEach { result << it }
assert result == [4, 3, 2, 1]
assert sortedSet.asReversed() == [4, 3, 2, 1] as Set
assert sortedSet.reversed() == [4, 3, 2, 1] as Set         // NEW

var map = [a: 1, b: 2]
result = []
map.reverseEach { k, v -> result << [k, v] }
assert result == [['b', 2], ['a', 1]]
assert map.reversed() == [b:2, a:1]                        // NEW

其他参考资料

结论

我们快速浏览了使用 JEP-431 功能与 Groovy 的方法。虽然 Groovy 已经提供了一些 JEP-431 提供的功能,但它看起来确实是对 JDK 的一个很好的补充。