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

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

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

List

use list.listIterator(list.size()).previous()

list.reversed()

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

Deque

use deque.descendingIterator()

deque.reversed()

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

NavigableSet

use set.descendingSet()

set.reversed()

set.reverseEach
set.asReversed()

Set (other)

N/A

set.reversed()

set.iterator().reverse()

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

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.