Sequenced Collections: A Deep Dive into JDK 21’s addition to Collections Framework
Sequenced collections introduced in Java 21 provide a unified way to work with ordered collections and make your code cleaner and more expressive while working with ordered collections. Some Collections in Java Collections framework has a defined encounter order which means they maintain the order of elements. For example, List
, Deque
and LinkedHashset
are ordered collections, but they all didn't have common semantics or a common super interface for this ordering till Java 21. Sequenced collections in Java 21 fill this gap by introducing three new interfaces: SequencedCollection
, SequencedSet
, and SequencedMap
. These interfaces provide a unified way to work with ordered collections, making it easier to write generic code that can handle any ordered collection. Also this introduces new methods to work with first and last elements, and a reversed()
method to get a reversed view of the collection.
Changes added in Sequenced Collections
Now let’s explore the new interfaces and methods introduced in JDK 21’s Sequenced Collections API.
1. SequencedCollection
This interface extends Collection
and adds methods for working with elements at both ends of an ordered collection:
public interface SequencedCollection<E> extends Collection<E> {
// Adds element at the end
default boolean addLast(E e);
// Adds element at the beginning
boolean addFirst(E e);
// Gets the first element
E getFirst();
// Gets the last element
E getLast();
// Removes and returns the first element
E removeFirst();
// Removes and returns the last element
E removeLast();
// Returns a reversed view of this collection
SequencedCollection<E> reversed();
}
2. SequencedSet
This interface extends both Set
and SequencedCollection
, representing an ordered collection that requires unique elements:
public interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
// Override to return a SequencedSet
@Override
SequencedSet<E> reversed();
}
3. SequencedMap
This interface extends Map
and adds methods for working with the first and last entries:
public interface SequencedMap<K, V> extends Map<K, V> {
// Map entry operations
Map.Entry<K, V> firstEntry();
Map.Entry<K, V> lastEntry();
Map.Entry<K, V> pollFirstEntry();
Map.Entry<K, V> pollLastEntry();
// Add entries at either end
default V putFirst(K k, V v);
default V putLast(K k, V v);
// Returns a reversed view
SequencedMap<K, V> reversed();
// Returns sequenced views of the map
SequencedSet<K> sequencedKeySet();
Collection<V> sequencedValues();
SequencedSet<Entry<K, V>> sequencedEntrySet();
}
Practical Examples
Now, let’s explore some practical examples of how these new interfaces can make your code cleaner and more expressive.
Example 1: Working with the First and Last Elements
Before JDK 21, getting the first or last element of a List required using indexed access:
// Before JDK 21
List<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie", "David"));
String first = names.get(0); // Get first element
String last = names.get(names.size() - 1); // Get last element
// Remove the first element
names.remove(0);
// Remove the last element
names.remove(names.size() - 1);
With JDK 21’s Sequenced Collections:
// With JDK 21
List<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie", "David"));
String first = names.getFirst(); // Cleaner syntax
String last = names.getLast(); // No need to calculate index
names.removeFirst(); // Simpler
names.removeLast(); // More intuitive
All these 4 methods getFirst()
, getLast()
, removeFirst()
, and removeLast()
are now available for all ordered collections, including List
, Deque
, LinkedHashSet
, NavigableSet
etc. since they all implement SequencedCollection
.
Example 2: Iterating in Reverse Order
Before JDK 21, iterating a collection in reverse order varied by collection type:
// For List - using ListIterator
List<String> list = Arrays.asList("A", "B", "C", "D");
ListIterator<String> iterator = list.listIterator(list.size());
while (iterator.hasPrevious()) {
System.out.println(iterator.previous());
}
// For Deque - using descendingIterator
Deque<String> deque = new ArrayDeque<>(Arrays.asList("A", "B", "C", "D"));
Iterator<String> descIterator = deque.descendingIterator();
while (descIterator.hasNext()) {
System.out.println(descIterator.next());
}
// For LinkedHashSet - not straightforward, usually required copying to another collection
LinkedHashSet<String> set = new LinkedHashSet<>(Arrays.asList("A", "B", "C", "D"));
List<String> reversedList = new ArrayList<>(set);
Collections.reverse(reversedList);
for (String item : reversedList) {
System.out.println(item);
}
With JDK 21, you can use the uniform reversed()
method:
// Works for any SequencedCollection
List<String> list = Arrays.asList("A", "B", "C", "D");
for (String s : list.reversed()) {
System.out.println(s); // Prints D, C, B, A
}
Deque<String> deque = new ArrayDeque<>(Arrays.asList("A", "B", "C", "D"));
for (String s : deque.reversed()) {
System.out.println(s); // Prints D, C, B, A
}
LinkedHashSet<String> set = new LinkedHashSet<>(Arrays.asList("A", "B", "C", "D"));
for (String s : set.reversed()) {
System.out.println(s); // Prints D, C, B, A
}
Example 3: Working with LinkedHashSet
Before JDK 21, reordering elements in a LinkedHashSet was difficult:
// Before JDK 21
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("A");
set.add("B");
set.add("C");
// Output: [A, B, C]
// To move B to the end - requires removing and re-adding
set.remove("B");
set.add("B");
// Output: [A, C, B]
// Getting first/last elements was complicated
Iterator<String> iterator = set.iterator();
String first = iterator.hasNext() ? iterator.next() : null;
// Getting last element was even worse - had to iterate through everything
String last = null;
for (String s : set) {
last = s;
}
With JDK 21:
// With JDK 21
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("A");
set.add("B");
set.add("C");
// Output: [A, B, C]
// Move B to the end
set.addLast("B"); // Automatically removes B from its original position and adds it at the end
// Output: [A, C, B]
// Getting first/last elements is straightforward
String first = set.getFirst(); // Returns "A"
String last = set.getLast(); // Returns "B"
Example 4: Working with Sequenced Maps
Before JDK 21, working with the first or last entries of an ordered map was cumbersome:
// Before JDK 21
LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
// Getting first entry
Map.Entry<Integer, String> firstEntry = null;
for (Map.Entry<Integer, String> entry : map.entrySet()) {
firstEntry = entry;
break;
}
// Getting last entry
Map.Entry<Integer, String> lastEntry = null;
for (Map.Entry<Integer, String> entry : map.entrySet()) {
lastEntry = entry;
}
With JDK 21:
// With JDK 21
LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
// Getting first and last entries is simple
Map.Entry<Integer, String> firstEntry = map.firstEntry(); // Entry(1, "One")
Map.Entry<Integer, String> lastEntry = map.lastEntry(); // Entry(3, "Three")
// Putting an entry at the beginning or end
map.putFirst(0, "Zero"); // Now: {0=Zero, 1=One, 2=Two, 3=Three}
map.putLast(4, "Four"); // Now: {0=Zero, 1=One, 2=Two, 3=Three, 4=Four}
// If key already exists, it gets moved to the requested position
map.putFirst(3, "Three"); // Moves key 3 to the beginning
// Now: {3=Three, 0=Zero, 1=One, 2=Two, 4=Four}
Example 5: Working with Reversed Views
The reversed()
method returns a view of the original collection with elements in reverse order. Any changes to the view affect the underlying collection, and vice versa:
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> reversed = list.reversed();
System.out.println(list); // [A, B, C]
System.out.println(reversed); // [C, B, A]
// Modifications to the reversed view affect the original list
reversed.set(0, "Z"); // Modifies the last element of original list
System.out.println(list); // [A, B, Z]
System.out.println(reversed); // [Z, B, A]
// Adding to the original affects the reversed view
list.add("D");
System.out.println(list); // [A, B, Z, D]
System.out.println(reversed); // [D, Z, B, A]
// Removing from the reversed view affects the original
reversed.remove(0); // Removes the last element from the original
System.out.println(list); // [A, B, Z]
System.out.println(reversed); // [Z, B, A]
Example 6: Creating a Reversed Stream
Before JDK 21, streaming a list in reverse order required somewhat complex code:
// Before JDK 21
List<String> list = Arrays.asList("A", "B", "C");
// Creating a reverse-ordered stream (verbose)
Stream<String> reversedStream = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(
new Iterator<String>() {
private final ListIterator<String> listIter = list.listIterator(list.size());
public boolean hasNext() {
return listIter.hasPrevious();
}
public String next() {
return listIter.previous();
}
},
Spliterator.ORDERED
),
false
);
With JDK 21:
// With JDK 21
List<String> list = Arrays.asList("A", "B", "C");
// Creating a reverse-ordered stream (clean)
Stream<String> reversedStream = list.reversed().stream();
When to Use Sequenced Collections
The Sequenced Collections API is particularly useful when:
- Writing generic algorithms that need to work with any ordered collection, regardless of whether it’s a List, Deque, LinkedHashSet, etc.
- Creating APIs that need to accept or return ordered collections without forcing a specific implementation.
- Working with the endpoints of collections, such as accessing the first or last element.
- Iterating in reverse without having to remember different approaches for different collection types.
Conclusion
The Sequenced Collections API provides a consistent and powerful way to work with ordered collections in Java.
To stay updated with the latest updates in Java and Spring, follow us on , , and Medium.