Sequenced Collections

Use the Java 21 SequencedCollection and SequencedMap methods to read, add and remove at both ends and to walk any ordered collection in reverse.

Syntaxlist.getFirst(); list.getLast(); list.addFirst(x); list.removeLast(); List<E> newestFirst = list.reversed(); map.firstEntry(); map.pollFirstEntry(); map.putFirst(k, v);

Java 21 added three interfaces for collections with a defined encounter order: SequencedCollection, SequencedSet and SequencedMap. Before them, getting the last element meant list.get(list.size() - 1) for a list, peekLast() for a deque and a full loop for a LinkedHashSet. Now one set of methods works on all of them.

The methods

SequencedCollection declares:

  • getFirst(), getLast()
  • addFirst(e), addLast(e)
  • removeFirst(), removeLast()
  • reversed(), a reverse-ordered view

SequencedMap adds firstEntry(), lastEntry(), pollFirstEntry(), pollLastEntry(), putFirst, putLast, reversed(), and the ordered views sequencedKeySet(), sequencedValues() and sequencedEntrySet().

Who implements them

InterfaceTypes
SequencedCollectionList, Deque and every SequencedSet
SequencedSetLinkedHashSet, SortedSet (so TreeSet)
SequencedMapLinkedHashMap, SortedMap (so TreeMap)

HashSet and HashMap have no defined order, so they do not implement these interfaces.

Details worth knowing

  • getFirst() on an empty collection throws NoSuchElementException, while get(0) on an empty list throws IndexOutOfBoundsException. Check isEmpty() first when empty is a normal case.
  • reversed() copies nothing. Changes to the original show through the view, and writes through a modifiable view change the original. Return List.copyOf(list.reversed()) if callers must not see later changes.
  • Sorted collections decide their own order, so addFirst and addLast on a TreeSet, and putFirst on a TreeMap, throw UnsupportedOperationException. Unmodifiable lists throw the same for every mutator.
  • On a LinkedHashSet, addFirst with an element already present moves it to the front, and putFirst on a LinkedHashMap moves an existing key in the same way.

Back-end angle

Typical uses are reading the latest event of an order's history, returning a newest-first list without copying and re-sorting, keeping a short list of recently viewed items with the most recent at the front, and taking the oldest pending entry from a LinkedHashMap with pollFirstEntry(). Methods can now accept SequencedCollection<T> when they need order but not indexes.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Read the latest status of an order with history.getLast() instead of indexing by size minus one.
  • Return an audit trail newest first with reversed() without copying or re-sorting the list.
  • Keep a de-duplicated recently viewed products list per session with LinkedHashSet.addFirst.

More examples

reversed() is a live view

The view and the original share the same data in both directions. Copy the reversed view when you need a value that stays fixed.

Example · java
void main() {
    List<Integer> ids = new ArrayList<>(List.of(1, 2, 3));
    List<Integer> newestFirst = ids.reversed();

    ids.add(4);
    IO.println("view after ids.add(4): " + newestFirst);

    newestFirst.addFirst(5);              // the front of the view is the end of ids
    IO.println("ids after view.addFirst(5): " + ids);

    List<Integer> snapshot = List.copyOf(ids.reversed());
    ids.add(6);
    IO.println("snapshot: " + snapshot);
}
// Output:
// view after ids.add(4): [4, 3, 2, 1]
// ids after view.addFirst(5): [1, 2, 3, 4, 5]
// snapshot: [5, 4, 3, 2, 1]

Recently viewed items and sorted sets

addFirst on a LinkedHashSet both de-duplicates and moves the item to the front, which is exactly what a recently viewed list needs. A TreeSet still gains getFirst and getLast but rejects positional inserts.

Example · java
void main() {
    LinkedHashSet<String> recent = new LinkedHashSet<>(List.of("/p/1", "/p/2", "/p/3"));
    recent.addFirst("/p/2");              // already present: moved to the front
    IO.println("recent: " + recent);
    IO.println("oldest: " + recent.getLast());

    TreeSet<Integer> scores = new TreeSet<>(List.of(70, 95, 82));
    IO.println("lowest " + scores.getFirst() + ", highest " + scores.getLast());
    try {
        scores.addFirst(100);
    } catch (UnsupportedOperationException e) {
        IO.println("TreeSet keeps its own order, so addFirst is unsupported");
    }
}
// Output:
// recent: [/p/2, /p/1, /p/3]
// oldest: /p/3
// lowest 70, highest 95
// TreeSet keeps its own order, so addFirst is unsupported

Empty collections and draining a SequencedMap

pollFirstEntry removes and returns the oldest entry in one call. The two exceptions differ, so a catch block written for get(0) will not catch getFirst.

Example · java
void main() {
    List<String> empty = new ArrayList<>();
    try {
        empty.getFirst();
    } catch (NoSuchElementException e) {
        IO.println("getFirst on empty: NoSuchElementException");
    }
    try {
        empty.get(0);
    } catch (IndexOutOfBoundsException e) {
        IO.println("get(0) on empty: IndexOutOfBoundsException");
    }

    SequencedMap<String, String> pending = new LinkedHashMap<>();
    pending.put("req-1", "GET /users");
    pending.put("req-2", "POST /orders");
    pending.put("req-3", "GET /health");

    var oldest = pending.pollFirstEntry();
    IO.println("handled " + oldest.getKey() + " " + oldest.getValue());
    IO.println("left: " + pending.sequencedKeySet());
    IO.println("newest first: " + pending.reversed());
}
// Output:
// getFirst on empty: NoSuchElementException
// get(0) on empty: IndexOutOfBoundsException
// handled req-1 GET /users
// left: [req-2, req-3]
// newest first: {req-3=GET /health, req-2=POST /orders}

Discussion

  • Be the first to comment on this lesson.