Groovy and Sequenced Collections (JEP-431)
Author: Paul King
Published: 2023-04-29 09:00AM
An exciting feature coming in JDK21 is 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 |
---|---|---|
|
|
|
|
|
|
|
|
requires iterating through the set |
|
|
|
JDK after JEP-431
After JEP-431, this improves greatly:
Collection Type | First element | Last element |
---|---|---|
|
|
|
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 |
---|---|---|
|
|
|
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 |
|
|
|
|
|
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
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 reverse
, reverseEach
and asReversed
extension methods.
The functionality isn’t universal however and sometimes catches folks out.
The reverse
method isn’t available for maps and sets. You need to
use e.g. the set’s iterator. Also, the standard reverse
produces
a new collection (or array) and there is an optional boolean parameter
which makes the method a mutating operation - reversing itself in-place.
This is in contrast to reversed()
from JEP-431 and asReversed()
which return a view.
Also, the reverseEach
and asReversed
are only provided for
NavigableSet
instances.
So, all in all, this functionality provided by JEP-431 is most welcome.
Collection Type | Before JEP-431 | After JEP-431 | Groovy |
---|---|---|---|
|
use |
|
|
|
use |
|
|
|
use |
|
|
|
N/A |
|
|
Examples
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
Additional References
-
Summary of features coming in JDK21 (Paul Krill on Infoworld)
-
Inside Java Newscast #45 (with Nicolai)
-
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)
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.