Order Matters: Exploring Java 21’s SequencedCollection Interface

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:

  • ArrayList
  • LinkedList
  • Vector
  • ArrayDeque
  • LinkedHashSet
  • TreeSet (as a SequencedSet)
  • 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:

  • LinkedHashSet
  • TreeSet

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

  1. Check for Empty Collections:
   // Always check before getFirst/getLast
if (!collection.isEmpty()) {
String first = collection.getFirst();
}
  1. 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
  1. Exception Handling:
   try {
collection.addFirst(element);
} catch (UnsupportedOperationException e) {
// Handle immutable collections
} catch (IllegalStateException e) {
// Handle capacity-restricted collections
}
  1. 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

  1. Consistent API: Unified methods across all ordered collections
  2. Reduced Boilerplate: No more manual first/last element access
  3. Improved Readability: Intent is clearer with getFirst() vs get(0)
  4. Enhanced Polymorphism: Write methods that work with any sequenced collection
  5. 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 SequencedCollection methods
  • Consider refactoring utility methods that manually access first/last elements
  • Watch for UnsupportedOperationException with 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper