blob: 51d31af5619cca9b6e9ffdbade092a930b50eefb [file] [log] [blame]
= Groovy and Sequenced Collections (JEP-431)
Paul King
:revdate: 2023-04-29T09:00:00+00:00
:keywords: groovy, jep431, collections
:description: This post looks at Groovy support for sequenced collections.
An exciting feature coming in JDK21 is
https://openjdk.org/jeps/431[Sequenced Collections]
which provide improved processing for collections which have
a defined encounter order. Additional details about the new
functionality can be found in the <<Additional References>> section later.
Since Groovy is designed to work very closely with the JDK libraries,
once JDK21 is released, Groovy will get the new functionality "for free".
Groovy however has its own solutions to some of the problems which JEP-431
addresses, so this post looks at the existing functionality (which you can use with older JDKs)
and the new functionality you can use once JDK21 is generally available,
and you upgrade to that JDK version.
The examples in this post use JDK21ea Build 20 (2023/4/27) and Groovy 4.0.11.
While EA builds come with numerous disclaimers warning that changes or removal
of functionality might occur before general release,
we'd expect the examples to work for subsequent releases.
We'll post an update if anything changes.
== Sequenced Collections Summary
Sequenced Collections adds three new interfaces: `SequencedSet`,
`SequencedCollection`, and `SequencedMap`. Each interface adds
some new methods which we'll encounter in later examples.
In the rest of this post, we'll explain the new sequenced collections functionality
by looking at various scenarios you might face when processing collections.
== Accessing the first and last element
=== JDK before JEP-431
Something as simple as accessing the first and last elements
for various collection types isn't consistent or as easy as might be expected (hence JEP-431).
Here are some examples of the JDK API calls you would use for this scenario:
|===
|Collection Type |First element |Last element
|`List`
|`list.get(0)`
|`list.get(list.size()-1)`
|`Deque`
|`deque.getFirst()`
|`deque.getLast()`
|`Set`
|`set.iterator().next()` or +
`set.stream().findFirst().get()`
| requires iterating through the set
|`SortedSet`
|`set.first()`
|`set.last()`
|===
=== JDK after JEP-431
After JEP-431, this improves greatly:
|===
|Collection Type |First element |Last element
|`List`, `Deque`, `Set`
|`collection.getFirst()`
|`collection.getLast()`
|===
=== Groovy before JEP-431
Groovy provides extension methods, `first()` and `last()`, for arrays and any `Iterable`, with
various optimised versions when it makes sense. The _subscript_ operator
is provided for any class having a `getAt` method. There are built-in implementations
for collections, arrays and numerous other classes.
|===
|Aggregate Type |First element |Last element
|`List`, `Deque`, `Set`, array
|`aggregate[0]` or `aggregate.first()`
|`aggregate[-1]` or `aggregate.last()`
|===
Groovy also provides _take_ extension methods which could be used here. You could use
`list.take(1)` to get a list containing just the first element of the original list,
and `takeRight(1)` for a list containing just the last element. These work across
all the different aggregate types too.
=== Groovy after JEP-431
No special support is yet added for JEP-431.
So Groovy functionality will be existing functionality
plus the new JDK functionality.
|===
|Aggregate Type |First element |Last element
|array
|`array[0]` +
`array.first()` +
|`array[-1]` +
`array.last()`
|`List`, `Deque`, `Set`
|`collection[0]` +
`collection.first()` +
`collection.getFirst()` +
`collection.first`
|`collection[-1]` +
`collection.last()` +
`collection.getLast()` +
`collection.last`
|===
For now, Groovy's approach provides uniformity also across arrays (and potentially other classes).
Folks should not feel any urgent pressure to use JEP-431 functionality for this scenario
with an important caveat. Over time, additional collection types may emerge which might
provide more efficient implementations for `getFirst()` or `getLast()` in which case it would
be beneficial to use those methods.
Future Groovy versions may provide specialised sequenced collections support.
The `first()` and `last()` extension methods may be implemented in terms
of `getFirst()` and `getLast()`.
We might also extend sequenced collection methods to arrays (and maybe other classes),
though it isn't a high priority for now.
=== Examples
[source,groovy]
----
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
----
== Removing first or last elements
If you need to mutate a collection, removing the first or last element,
Groovy doesn't offer consistent extension methods across all the aggregate types.
You can use the JDK `remove(0)` method from `List` to remove the first element from the list (and Groovy also provides a nice `removeAt(0)` alias).
Groovy also provides `removeLast()` for lists.
Given this, the `removeFirst()` and `removeLast()`
methods from `SequencedCollection` are a nice addition.
If you want to create a new aggregate which is the same as the original
but with the first (or last) element removed, Groovy provides
`tail()` and `drop(1)` (or `init()` and `dropRight(1)`).
== Adding elements to the front/end
If you need to mutate a collection, adding elements at the front or end,
Groovy doesn't offer consistent extension methods across all the aggregate types.
You'd normally use `add(element)` or `add(0, element)` for lists.
So the `addFirst()` and `addLast()`
methods from `SequencedCollection` are a nice addition.
Groovy does offer the `leftShift` operator (`<<`) as another way to append to the end of a list.
== Working with reversed collections
Another area tackled by JEP-431 is improved consistency for
working with a collection in reverse order.
Groovy already offers some enhancements for this scenario
with `reverseEach` and `asReversed` extension methods.
These methods aren't available for all sets, e.g. not for `LinkedHashSet`
but only `NavigableSet` instances. Also, the `asReversed` method
creates a new collection rather than a view that is provided by
JEP-431s `reversed()` method. There are times when the latter might be preferred.
So, all in all, this functionality provided by JEP-431 is most welcome.
|===
|Collection Type |Before JEP-431 |After JEP-431 |Groovy
|`List`
|use `list.listIterator(list.size()).previous()`
| `list.reversed()`
| `list.reverseEach` +
`list.asReversed()`
|`Deque`
|use `deque.descendingIterator()`
|`deque.reversed()`
| `deque.reverseEach` +
`deque.asReversed()`
|`NavigableSet`
|use `set.descendingSet()`
|`set.reversed()`
| `set.reverseEach` +
`set.asReversed()`
|`Set`
|N/A
|`set.reversed()`
|N/A
|===
=== Examples
[source,groovy]
----
var result = []
list.reverseEach { result << it }
assert result == [3, 2, 1]
assert list.asReversed() == [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.reversed() == [3, 2, 1] // NEW
result = []
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
----
== Additional References
* https://openjdk.org/jeps/431[JEP-431 Proposal]
* https://www.infoworld.com/article/3689880/jdk-21-the-new-features-in-java-21.html[Summary of features coming in JDK21] (Paul Krill on Infoworld)
* https://www.youtube.com/watch?v=9G_0el3RWPE[Inside Java Newscast #45] (with Nicolai)
* https://inside.java/2023/04/25/podcast-031/[Inside Java Podcast Episode 31] (Ana-Maria Mihalceanu with Stuart Marks)
* https://www.infoq.com/news/2023/03/collections-framework-makeover/[] (A N M Bazlur Rahman on InfoQ)
* https://groovy.apache.org/blog/groovy-list-processing-cheat-sheet[Groovy list processing cheat sheet]
== Conclusion
We have had a quick look at using JEP-431 functionality with Groovy.
While Groovy already offers some of the functionality which JEP-431 provides,
it certainly looks like a nice addition to the JDK.