JDK JDK 学习手册8 → 27 · JDK 专用学习站
DEEPDIVE · COLLECTIONS · JEP 431

SequencedCollection:集合终于有了「两端」

JDK 21 · 深钻主题 03 / 08 · 预计 45 分钟
核心四步深钻JEP 431
一句话JDK 21 为 List、Deque、LinkedHashSet、SortedSet、LinkedHashMap、SortedMap 补上了统一的「首尾」API:addFirst、addLast、getFirst、getLast、reversed。以前每个实现各写一套,现在一套接口覆盖。
本质:集合的核心问题从「有哪些元素」扩展到「元素按什么顺序排列」。SequencedCollection 把「遇到顺序」提升为一等公民,并提供零拷贝的反向视图。

学完你要能自己回答

  1. SequencedCollection 和 List、Deque 是什么关系?为什么不是所有 Collection 都实现它?
  2. reversed() 返回视图还是副本?修改视图会影响原集合吗?
  3. LinkedHashSet.addFirst 对已存在元素做了什么?TreeSet.addFirst 为什么抛异常?
  4. SequencedMap 的 putFirst 与 LinkedHashMap 的插入顺序如何相互影响?
  5. 什么场景应该用 reversed(),什么场景仍应使用 Collections.reverse()?
01 从「一堆元素」到「有方向的一排」02 反向视图:同一份数据,两个观察方向

从「一堆元素」到「有方向的一排」01

在 JDK 21 之前,「有序集合」是个模糊说法:List 有索引,Deque 有首尾方法,LinkedHashSet 有插入顺序但没有 getFirst,TreeSet 有 first()/last() 但方法名不统一。想写一个「取首元素」的通用方法,只能对每种类型分别判断。

SequencedCollection 把这些能力抽象成一个方向明确的一排元素:左端(first)与右端(last)。所有实现共享同一组方法名,reversed() 还能得到一个反向视图。

接口 / 类有序依据addFirst / addLastreversed()
List(ArrayList/LinkedList)索引顺序插入到两端,支持反向视图
Deque(ArrayDeque/LinkedList)双端队列顺序支持反向视图
LinkedHashSet插入顺序(或访问顺序)支持;已存在元素会被移动到端点反向视图
SortedSet / TreeSet比较器顺序addFirst/addLast 抛 UOE反向视图
LinkedHashMap插入顺序(或访问顺序)putFirst/putLast 移动条目反向视图
SortedMap / TreeMap键的比较器顺序putFirst/putLast 抛 UOE反向视图
「有序」不等于「可任意改序」。TreeSet 的顺序由比较器决定,所以它实现了 SequencedCollection 的读取方法,但拒绝在两端插入。

反向视图:同一份数据,两个观察方向02

reversed() 不是复制。它返回一个视图:读和写都落到原集合上,只是迭代方向相反。

List<String> list = new ArrayList<>(List.of("a", "b", "c"));
List<String> rev = list.reversed();

System.out.println(rev);        // [c, b, a]
rev.add("z");                   // 加到反向视图的末尾 = 原列表开头?
System.out.println(list);       // [z, a, b, c]
System.out.println(list.getFirst()); // z

上例中,rev.add("z") 等价于原列表的 addFirst("z"),因为反向视图的「末尾」就是原集合的「开头」。这种方向映射是理解所有 reversed 操作的关键。

  • Collections.reverse(list) 原地反转,会修改元素位置;list.reversed() 只改变观察方向。
  • 视图上的 addFirst 等于原集合的 addLast,反之亦然。
  • 视图是轻量对象,创建成本低,适合在方法间传递「从后往前看」的视角。
01 接口家族与默认方法02 四个实现的真实行为03 空集合与 null 的边界

接口家族与默认方法01

public interface SequencedCollection<E> extends Collection<E> {
    void addFirst(E e);          // 默认抛 UnsupportedOperationException
    void addLast(E e);           // 默认抛 UnsupportedOperationException
    E getFirst();                // 默认用 iterator().next()
    E getLast();                 // 默认遍历到最后一个
    E removeFirst();             // 默认用 iterator().remove()
    E removeLast();              // 默认遍历到最后一个再删
    SequencedCollection<E> reversed();
}

public interface SequencedSet<E> extends SequencedCollection<E>, Set<E> {
    SequencedSet<E> reversed();  // 协变返回
}

public interface SequencedMap<K,V> extends Map<K,V> {
    Map.Entry<K,V> firstEntry();
    Map.Entry<K,V> lastEntry();
    Map.Entry<K,V> pollFirstEntry();
    Map.Entry<K,V> pollLastEntry();
    V putFirst(K k, V v);
    V putLast(K k, V v);
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Map.Entry<K,V>> sequencedEntrySet();
    SequencedMap<K,V> reversed();
}
  • addFirst/addLast 的默认实现直接抛 UOE,因此不支持改序的实现(TreeSet/TreeMap)无需重写就自动拒绝。
  • getFirst/getLast/removeFirst/removeLast 有基于迭代器的默认实现;空集合时 get/remove 抛 NoSuchElementException。
  • SequencedMap 没有继承 SequencedCollection,因为 Map 不是 Collection;它单独提供 sequenced 视图。
  • LinkedHashSet、LinkedHashMap 还额外实现了 SequencedSet / SequencedMap,并覆盖了端点操作。

四个实现的真实行为02

ArrayList:端点操作可能触发扩容和搬移

addFirst 需要把已有元素整体右移一位,复杂度 O(n);addLast 均摊 O(1)。频繁在头部插入时,ArrayDeque 更合适。

LinkedHashSet:addFirst 是「移动」,不是「重复添加」

LinkedHashSet<String> set = new LinkedHashSet<>(List.of("a", "b", "c"));
set.addFirst("c");
System.out.println(set); // [c, a, b] —— c 被移动到最前,没有重复

LinkedHashMap:putFirst 同时更新值和顺序

LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.putFirst("b", 20);
System.out.println(map); // {b=20, a=1}
System.out.println(map.get("b")); // 20

TreeSet / TreeMap:顺序由比较器决定

TreeSet<Integer> set = new TreeSet<>(List.of(2, 1));
try {
    set.addFirst(0);
} catch (UnsupportedOperationException e) {
    System.out.println("顺序由比较器决定,不能插到端点");
}

TreeSet 的 first/last 是「最小/最大」而非「最早/最晚插入」。对它调用 addFirst 会破坏排序不变式,所以接口选择抛异常而不是默默重排。

空集合与 null 的边界03

操作空集合行为说明
getFirst / getLastNoSuchElementException没有元素可取
removeFirst / removeLastNoSuchElementException没有元素可删
pollFirstEntry / pollLastEntry返回 nullMap 的轮询语义
firstEntry / lastEntry返回 nullMap 的查询语义
addFirst(null)(LinkedList)允许,取决于实现ArrayList 允许 null;不可变集合抛 UOE

List.of() 等不可变集合实现了 SequencedCollection,但所有修改方法都抛 UOE。读取端点仍然可用(只要非空)。

01 四个常见配方02 可运行示例:两端与反向视图

四个常见配方01

1. 取最近 N 条记录

Deque<LogEvent> recent = new ArrayDeque<>();
recent.addFirst(event);
if (recent.size() > 100) {
    recent.removeLast();
}

2. 从后往前遍历,但不动数据

for (String s : list.reversed()) {
    System.out.println(s);
}

3. 让 LinkedHashMap 同时支持两端插入

LinkedHashMap<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true);
lru.put("a", 1);
lru.put("b", 2);
lru.get("a");          // 访问顺序:b, a
lru.putFirst("b", 2);  // 手动把 b 移到最前:b, a

4. 只读视图

SequencedCollection<String> readOnly =
        Collections.unmodifiableSequencedCollection(list);
readOnly.getFirst();   // 可以读
readOnly.addFirst("x"); // UnsupportedOperationException

可运行示例:两端与反向视图02

import java.util.*;

public class SequencedDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("b", "c"));
        list.addFirst("a");
        list.addLast("d");
        System.out.println("list=" + list);
        System.out.println("first=" + list.getFirst() + ", last=" + list.getLast());
        System.out.println("reversed=" + list.reversed());

        LinkedHashSet<String> set = new LinkedHashSet<>(List.of("a", "b", "c"));
        set.addFirst("c");
        System.out.println("set=" + set);

        LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
        map.put("a", 1);
        map.put("b", 2);
        map.putFirst("b", 20);
        System.out.println("map=" + map);
        System.out.println("firstEntry=" + map.firstEntry());

        TreeMap<String, Integer> tree = new TreeMap<>(Map.of("b", 2, "a", 1));
        System.out.println("treeFirst=" + tree.firstEntry());
        try {
            tree.putFirst("z", 26);
        } catch (UnsupportedOperationException e) {
            System.out.println("tree.putFirst -> UOE");
        }
    }
}
list=[a, b, c, d]
first=a, last=d
reversed=[d, c, b, a]
set=[c, a, b]
map={b=20, a=1}
firstEntry=b=20
treeFirst=a=1
tree.putFirst -> UOE
01 先猜再看02 费曼自检:接口与实现

先猜再看01

先猜再看list.reversed().add("x") 会把 x 加到原列表的头部还是尾部?
头部。反向视图的末尾对应原列表的开头,所以视图上的 add 等于原列表的 addFirst。
先猜再看LinkedHashSet.addFirst("c") 在 c 已存在时,集合大小会变吗?
不会。它把已有元素移动到最前,不新增元素;迭代顺序变为 c 开头。
先猜再看TreeSet 的 getFirst() 返回最早插入的元素吗?
不是。它返回比较器意义下的最小元素。TreeSet 不记录插入时间,顺序完全由排序规则决定。

费曼自检:接口与实现02

01. 为什么 SequencedCollection 不直接把方法加进 Collection?
并非所有 Collection 都有定义明确的遇到顺序(如 HashSet),强行加入会迫使实现抛出大量 UOE,破坏接口契约。用子接口按能力分层更清晰。
02. List、Deque、LinkedHashSet、TreeSet 都实现 SequencedCollection,它们的「顺序」含义一样吗?
不一样:List 是索引顺序,Deque 是双端队列顺序,LinkedHashSet 是插入/访问顺序,TreeSet 是比较器顺序。接口统一的是端点操作,不是排序语义。
03. reversed() 和 Collections.reverse() 的区别?
前者是零拷贝反向视图,底层数据不变;后者原地修改列表元素顺序。需要持久改序用 reverse,需要临时反向遍历用 reversed。
04. SequencedMap 的 firstEntry 和 keySet().iterator().next() 有什么区别?
firstEntry 返回包含键值的 Map.Entry(可能为 null),语义更直接;迭代器方式需要先判空,且不能直接拿到值。firstEntry 在空 Map 上返回 null,不抛异常。
05. 给一个应该用 ArrayDeque 而不是 ArrayList.addFirst 的场景。
需要频繁在头部插入/删除(如滑动窗口、栈、最近记录),ArrayDeque 的头部操作均摊 O(1),ArrayList.addFirst 每次要搬移 O(n) 个元素。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。