| = 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. |