Looking at Java 21: Sequenced Collections

 · 5 min
Photo by Vedrana Filipović on Unsplash

Dealing with Collections is improving with Java 21, as three new interfaces get retro-fitted right into the existing type hierarchies. These Sequenced Collections give us a uniform API to access the first and last elements, and process Collections in reverse.

To better understand what Sequenced Collections are, let’s take a look a what Collections were before.


The Java Collections Framework

Introduced in Java 1.2, the Collections Framework gave use many interfaces and classes to represent groups of objects. This unified API has many advantages, like enabling interoperability between unrelated APIs with “standardized” Collection types, and therefore, fostering code reuse.

Over a dozen interfaces provide an excellent abstraction for the underlying general- and special-purpose implementations, including implementation for concurrent environments. Even though the type hierarchies and functionality of the different Collection types is well-thought-out and give a lot of flexibility and functionality, a certain aspect is missing… until now!

Front to Back, Back to Front

A quite common task with Collections is getting the first or last element, and usually, it’s not pretty… I’m pretty sure everyone reading this article has done the following countless times:

java
List<String> items = ...;

String first = items.get(0);
String last = items.get(items.size() - 1);

Some Collection types support accessing the first and last element directly, but there’s no shared interface with a common API (yet):

Collection TypeFirst ElementLast Element
Listlist.get(0)list.get(list.size() - 1)
Dequedeque.getFirst()deque.getLast()
SortedSetsortedSet.first()sortedSet.last()

The first element is also accessible through the Iterator<E>, too, but it feels like a cheat:

java
List<String> items = ...;

String first = items.iterator().next();

Every Collection-based type is easily traversable front to back, as it’s a descendant of Iterator. This way, we can use for-each loops, Stream pipelines, and create arrays from any collection by calling toArray(). For the inverse direction, back to front, there’s no easy way or trick available. That means, for example, that if we have to deal with a LinkedHashSet, we have to traverse the whole collection to get to the last element.

Until Java 21 and Sequenced Collections!


Sequenced Collection Types

JEP 431 introduces three new interfaces into the type hierarchy of the collection framework at strategic positions. Like many other features over the years, these interfaces inject their new functionality with the help of default methods, so everything is still backward compatible, and no one is forced to implement the methods by themselves unless we want a more specialized implementation.

The three new sequenced interfaces are:

  • SequencedCollection<E> extends Collection<E>
  • SequencedSet<E> extends SequencedCollection<E>, Set<E>
  • SequencedMap<K, V> extends Map<K, V>

SequencedCollection

The SequencedCollection<E> interface looks like you would expect it, especially since all its methods, with the exception of reversed(), were promoted from the pre-existing Deque<E> type, to provide a known and uniform API:

java
interface SequencedCollection<E> extends Collection<E> {

    // NEW METHOD

    SequencedCollection<E> reversed();


    // PROMOTED METHODS FROM Deque<E>

    void addFirst(E);
    void addLast(E);

    E getFirst();
    E getLast();

    E removeFirst();
    E removeLast();
}

The add... and remove... methods are optional and throw an UnsupportedOperationException in their default implementation, to support unmodifiable collections. The get... methods behave like their brethren by throwing a NoSuchElementException in the case of an empty collection.

SequencedSet

The SequencedSet<E> is based on SequencedCollection<E> but has a covariant override for the return type of the reversed() method:

java
interface SequencedSet<E> extends SequencedCollection<E>, Set<E> {

    SequencedSet<E> reversed();
}

The mentioned behavior from before is also valid, as none of the default implementations from SequencedCollection<E> are overridden.

SequencedMap<K, V>

Although differ conceptionally from other Collections as key-value-based constructs, there’s still a lot to gain from a sequenced approach. Like with SequencedCollection<E>, the type SequencedMap<K, V> gains some of its functionality from promoting methods from a pre-existing type, in this case, NavigableMap<K, V>:

java
interface SequencedMap<K,V> extends Map<K,V> {

    // NEW METHODS

    SequencedMap<K,V> reversed();

    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();

    V putFirst(K, V);
    V putLast(K, V);


    // PROMOTED METHODS FROM NavigableMap<K, V>
    
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

An interesting addition is the two promoted poll... methods, as they give us an easy way to access the first or last entry AND remove it in one swoop.


Extending Java’s Collection Framework

As mentioned before, the three new types are retrofitted right into the pre-existing type hierarchy, to give us all the new goodies without breaking compatibility in any way:

Sequenced Collections Type Hierarchy (<a href="https://openjdk.org/jeps/431">JEP 431</a>)
Sequenced Collections Type Hierarchy (JEP 431)

The Collection-based types change as follows:

  • Both List and Deque now share SequencedCollection as their immediate super-interface.
  • SortedSet now is a direct descendant of SequencedSet
  • The implementation LinkedHashSet is now also a SequencedSet. additionally implements SequencedSet

The changes for Map were not as numerous, the type SequenceMap is directly under Map and above is the new super-interface for SorterMap and an additional interface for the implementation LinkedHashMap.

To match the general theme of the Collections Framework, there are also three new static helper methods available on Collections:

  • Collections.unmodifiableSequencedCollection(sequencedCollection)
  • Collections.unmodifiableSequencedSet(sequencedSet)
  • Collections.unmodifiableSequencedMap(sequencedMap)

Conclusion

The introduction of a well-defined encounter order and a uniform API across the board is a welcomed addition to Java in my opinion. It will provide developers with a more straightforward way to simplify commons collection tasks, and little by little, Java adds more convenience to its types.

As I talked about in previous articles, it might not be as concise and across-the-board compared to other languages, but implementing it in a straightforward and backward-compatible way is a trade-off I gladly accept.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!

Resources

Looking at Java 21