DEEPDIVE · COLLECTIONS · JEP 431
SequencedCollection:集合终于有了「两端」
JDK 21 · 深钻主题 03 / 08 · 预计 45 分钟
一句话JDK 21 为 List、Deque、LinkedHashSet、SortedSet、LinkedHashMap、SortedMap 补上了统一的「首尾」API:addFirst、addLast、getFirst、getLast、reversed。以前每个实现各写一套,现在一套接口覆盖。
本质:集合的核心问题从「有哪些元素」扩展到「元素按什么顺序排列」。SequencedCollection 把「遇到顺序」提升为一等公民,并提供零拷贝的反向视图。
学完你要能自己回答
- SequencedCollection 和 List、Deque 是什么关系?为什么不是所有 Collection 都实现它?
reversed()返回视图还是副本?修改视图会影响原集合吗?- LinkedHashSet.addFirst 对已存在元素做了什么?TreeSet.addFirst 为什么抛异常?
- SequencedMap 的 putFirst 与 LinkedHashMap 的插入顺序如何相互影响?
- 什么场景应该用 reversed(),什么场景仍应使用 Collections.reverse()?
01 从「一堆元素」到「有方向的一排」02 反向视图:同一份数据,两个观察方向
从「一堆元素」到「有方向的一排」01
在 JDK 21 之前,「有序集合」是个模糊说法:List 有索引,Deque 有首尾方法,LinkedHashSet 有插入顺序但没有 getFirst,TreeSet 有 first()/last() 但方法名不统一。想写一个「取首元素」的通用方法,只能对每种类型分别判断。
SequencedCollection 把这些能力抽象成一个方向明确的一排元素:左端(first)与右端(last)。所有实现共享同一组方法名,reversed() 还能得到一个反向视图。
| 接口 / 类 | 有序依据 | addFirst / addLast | reversed() |
|---|---|---|---|
| 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 / getLast | NoSuchElementException | 没有元素可取 |
| removeFirst / removeLast | NoSuchElementException | 没有元素可删 |
| pollFirstEntry / pollLastEntry | 返回 null | Map 的轮询语义 |
| firstEntry / lastEntry | 返回 null | Map 的查询语义 |
| 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) 个元素。
学习进度
记录保存在本机浏览器 · 不上传