Java's collections framework has long provided various data structures with different ordering characteristics, but until recently, it lacked a unified interface for collections that maintain a defined sequence. Java 21 addressed this gap with the introduction of the SequencedCollection interface in JEP 431, bringing consistency and new capabilities to ordered collections.
This article explores the SequencedCollection interface, its methods, and how it simplifies working with collections that have a well-defined encounter order.
The Problem: Inconsistent Access Patterns
Before Java 21, accessing the first and last elements of different ordered collections required different approaches:
// For List (has getFirst() and getLast() in some versions?)
List<String> list = new ArrayList<>();
if (!list.isEmpty()) {
String first = list.get(0);
String last = list.get(list.size() - 1);
}
// For Deque (has direct methods)
Deque<String> deque = new ArrayDeque<>();
String first = deque.getFirst(); // Consistent
String last = deque.getLast(); // Consistent
// For LinkedHashSet (no direct methods)
LinkedHashSet<String> set = new LinkedHashSet<>();
if (!set.isEmpty()) {
String first = set.iterator().next();
// Getting last element was particularly cumbersome
String last = null;
for (String item : set) {
last = item;
}
}
This inconsistency made code harder to write, read, and maintain when working with different collection types.
What is SequencedCollection?
SequencedCollection is a new interface in Java 21 that represents a Collection with a well-defined encounter order. It provides uniform access to the first and last elements, as well as reversed views of the collection.
Key Characteristics:
- Defines a encounter order (not necessarily sorted)
- Provides bidirectional access (first and last elements)
- Offers reversed view of the collection
- Implemented by all ordered collections in Java
Interface Hierarchy:
Collection<E> ↓ SequencedCollection<E> ↓ List<E>, Deque<E>, LinkedHashSet<E>, etc.
Core Methods of SequencedCollection
The interface defines six essential methods:
public interface SequencedCollection<E> extends Collection<E> {
// New methods
SequencedCollection<E> reversed();
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
Implementing Collections
Here are the main collections that implement SequencedCollection:
ArrayListLinkedListVectorArrayDequeLinkedHashSetTreeSet(as aSequencedSet)Stack
And the immutable collections from Collections utility class:
List.copyOf()Set.copyOf()Collections.unmodifiableList()Collections.unmodifiableSet()
Practical Examples
Example 1: Basic Usage with Different Collections
import java.util.*;
public class SequencedCollectionExamples {
public static void main(String[] args) {
// Working with ArrayList
SequencedCollection<String> list = new ArrayList<>();
list.add("B");
list.addFirst("A"); // Adds to beginning
list.addLast("C"); // Adds to end
System.out.println("List: " + list); // [A, B, C]
System.out.println("First: " + list.getFirst()); // A
System.out.println("Last: " + list.getLast()); // C
System.out.println("Reversed: " + list.reversed()); // [C, B, A]
// Working with LinkedHashSet
SequencedCollection<String> set = new LinkedHashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Cherry");
System.out.println("Set first: " + set.getFirst()); // Apple
System.out.println("Set last: " + set.getLast()); // Cherry
}
}
Example 2: Processing Elements from Both Ends
public class BidirectionalProcessing {
public static void main(String[] args) {
SequencedCollection<Integer> numbers = new ArrayDeque<>();
for (int i = 1; i <= 5; i++) {
numbers.addLast(i);
}
// Process from both ends towards center
while (!numbers.isEmpty()) {
if (!numbers.isEmpty()) {
System.out.println("First: " + numbers.removeFirst());
}
if (!numbers.isEmpty()) {
System.out.println("Last: " + numbers.removeLast());
}
}
}
}
Output:
First: 1 Last: 5 First: 2 Last: 4 First: 3
Example 3: Using Reversed View
public class ReversedExample {
public static void main(String[] args) {
SequencedCollection<String> colors = new ArrayList<>(
List.of("Red", "Green", "Blue")
);
// Get reversed view (doesn't modify original)
SequencedCollection<String> reversed = colors.reversed();
System.out.println("Original: " + colors); // [Red, Green, Blue]
System.out.println("Reversed: " + reversed); // [Blue, Green, Red]
// Changes in original reflect in reversed view
colors.addFirst("Yellow");
System.out.println("After addFirst:");
System.out.println("Original: " + colors); // [Yellow, Red, Green, Blue]
System.out.println("Reversed: " + reversed); // [Blue, Green, Red, Yellow]
// Changes through reversed view affect original
reversed.addFirst("Purple");
System.out.println("After reversed.addFirst:");
System.out.println("Original: " + colors); // [Yellow, Red, Green, Blue, Purple]
System.out.println("Reversed: " + reversed); // [Purple, Blue, Green, Red, Yellow]
}
}
SequencedSet: The Sorted Cousin
Java 21 also introduced SequencedSet which extends both Set and SequencedCollection:
public interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed(); // Covariant override
}
Key implementations:
LinkedHashSetTreeSet
Example with TreeSet:
public class SequencedSetExample {
public static void main(String[] args) {
SequencedSet<Integer> sortedSet = new TreeSet<>();
sortedSet.add(3);
sortedSet.add(1);
sortedSet.add(2);
System.out.println("Set: " + sortedSet); // [1, 2, 3] - sorted!
System.out.println("First: " + sortedSet.getFirst()); // 1
System.out.println("Last: " + sortedSet.getLast()); // 3
System.out.println("Reversed: " + sortedSet.reversed()); // [3, 2, 1]
}
}
Immutable Collections and SequencedCollection
The SequencedCollection methods work with immutable collections but throw UnsupportedOperationException for mutating operations:
public class ImmutableExample {
public static void main(String[] args) {
// Immutable list
SequencedCollection<String> immutable = List.of("A", "B", "C");
System.out.println("First: " + immutable.getFirst()); // A
System.out.println("Last: " + immutable.getLast()); // C
System.out.println("Reversed: " + immutable.reversed()); // [C, B, A]
try {
immutable.addFirst("X"); // Throws UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify immutable collection");
}
}
}
Comparison with Traditional Approaches
Before Java 21:
// Getting first and last from List was verbose
public <T> void processEnds(List<T> list) {
if (list.isEmpty()) return;
T first = list.get(0);
T last = list.get(list.size() - 1);
// Process first and last
}
// Getting first and last from Set was even worse
public <T> void processEnds(LinkedHashSet<T> set) {
if (set.isEmpty()) return;
T first = set.iterator().next();
T last = null;
for (T item : set) {
last = item;
}
// Process first and last
}
With Java 21:
// Unified approach for all sequenced collections
public <T> void processEnds(SequencedCollection<T> collection) {
if (collection.isEmpty()) return;
T first = collection.getFirst();
T last = collection.getLast();
// Process first and last - works for ALL sequenced collections!
}
Best Practices and Considerations
- Check for Empty Collections:
// Always check before getFirst/getLast
if (!collection.isEmpty()) {
String first = collection.getFirst();
}
- Understand Reversed Views:
reversed()returns a view, not a copy- Changes in original affect reversed view and vice versa
- No additional memory overhead for the view
- Exception Handling:
try {
collection.addFirst(element);
} catch (UnsupportedOperationException e) {
// Handle immutable collections
} catch (IllegalStateException e) {
// Handle capacity-restricted collections
}
- Use Most Specific Type:
// Prefer
void processList(List<String> list) { }
// Over
void processList(SequencedCollection<String> collection) { }
// When you specifically need sequenced operations
void processEnds(SequencedCollection<String> collection) { }
Benefits of SequencedCollection
- Consistent API: Unified methods across all ordered collections
- Reduced Boilerplate: No more manual first/last element access
- Improved Readability: Intent is clearer with
getFirst()vsget(0) - Enhanced Polymorphism: Write methods that work with any sequenced collection
- Bidirectional Processing: Easy access to both ends of collections
Migration Considerations
For codebases migrating to Java 21+:
- Existing code continues to work unchanged
- Can gradually adopt
SequencedCollectionmethods - Consider refactoring utility methods that manually access first/last elements
- Watch for
UnsupportedOperationExceptionwith immutable collections
Conclusion
The SequencedCollection interface in Java 21 fills a long-standing gap in the Collections Framework by providing a consistent, intuitive API for collections with defined encounter order. It brings:
- Standardized access to first and last elements
- Bidirectional operations for all ordered collections
- Reversed views without copying data
- Polymorphic handling of different collection types
By adopting SequencedCollection in your code, you can write cleaner, more maintainable, and more expressive code when working with ordered collections, while enjoying the benefits of a unified API across ArrayList, LinkedList, LinkedHashSet, ArrayDeque, and other ordered collections.