JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · COLLECTIONS · SEQUENCED

集合与 SequencedCollection

JDK 21 · 速查主题 02 / 12 · 预计 25 分钟
核心25 分钟JEP 431
一句话JDK 21 给集合框架补上了迟到三十年的首尾抽象:SequencedCollection / SequencedSet / SequencedMap 统一了 getFirst、addLast、reversed 等操作,让 List、Deque、LinkedHashSet、SortedSet 第一次共享同一套顺序 API。

学完你要能自己回答

  1. SequencedCollection 解决的是什么历史问题?它和 List、Deque 是什么关系?
  2. reversed() 返回的是拷贝还是视图?对反转后的集合写入,原集合会变吗?
  3. putFirst 在 LinkedHashMap 和 TreeMap 上行为一样吗?为什么?

选型:先确定访问模式01

集合选型只需要回答四个问题:是否需要顺序、是否允许重复、是否需要按键查找、是否需要并发。大部分性能问题不是集合选错,而是用错访问模式(例如在 ArrayList 头部频繁插入)。

实现底层结构顺序随机访问典型场景
ArrayList动态数组插入序O(1)最常用;尾部追加、遍历
LinkedList双向链表插入序O(n)几乎不推荐;需要 Deque 时用 ArrayDeque
ArrayDeque循环数组插入序仅首尾栈、队列、双端队列首选
HashSet哈希表无保证—去重、存在性判断
LinkedHashSet哈希表 + 链表插入序—去重但保持顺序
TreeSet红黑树排序序—需要有序或范围查询
HashMap哈希表无保证—通用键值存储
LinkedHashMap哈希表 + 链表插入/访问序—LRU 缓存、保序输出
TreeMap红黑树键排序—范围查询、有序遍历
ConcurrentHashMap分段/桶级并发无保证—多线程共享映射
LinkedList 的 get(i) 是 O(n),在循环里按下标访问会退化成 O(n²)。需要双端队列时用 ArrayDeque;需要 List 语义时用 ArrayList。

工厂方法与不可变集合02

JDK 9 引入 List.of / Set.of / Map.of,一行创建不可变集合。它们拒绝 null、拒绝重复 key,并在创建时拷贝元素(不是视图)。

List<String> list = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Map<String, Integer> big = Map.ofEntries(
    Map.entry("a", 1),
    Map.entry("b", 2),
    Map.entry("c", 3)
);

list.add("d");            // UnsupportedOperationException
List.of("a", null);       // NullPointerException
Set.of(1, 1);             // IllegalArgumentException: duplicate element
Map.of("a", 1, "a", 2);   // IllegalArgumentException: duplicate key
  • List.of 保留参数顺序;Set.of 的迭代顺序不保证,不要依赖。
  • 不可变的是结构:元素对象本身仍可能可变。需要深不可变时自行拷贝。
  • List.copyOf(collection) 在入参已经是不可变 List 时可能直接返回同一个实例;不要依赖这一点。
  • Arrays.asList 是固定长度视图,支持 set 但不支持增删,且会写回原数组;List.of 才是真正不可变。
String[] arr = {"a", "b"};
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "z");
System.out.println(Arrays.toString(arr)); // [z, b] —— 会写回原数组

List<String> immutable = List.of(arr);
// immutable.set(0, "x"); // UnsupportedOperationException

SequencedCollection:统一首尾操作03

在 JDK 21 之前,首尾操作分散在 List(get(0)、get(size-1))、Deque(getFirst、addLast)和 SortedSet(first、last)里。SequencedCollection 把这些能力抽象成统一接口。

方法语义对 List对 Deque对 LinkedHashSet
getFirst()返回第一个元素index 0队首最早插入
getLast()返回最后一个元素index size-1队尾最新插入
addFirst(e)加到最前插到 index 0入队首移到最前(去重)
addLast(e)加到最后追加入队尾追加/移到末尾
removeFirst()删除并返回第一个删 index 0出队首删最早
removeLast()删除并返回最后一个删末尾出队尾删最新
reversed()反转顺序的视图视图视图视图
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
System.out.println(list.getFirst()); // a
System.out.println(list.getLast());  // c

SequencedCollection<String> rev = list.reversed();
System.out.println(rev);             // [c, b, a]

rev.addFirst("z");                   // 等价于原 list.addLast("z")
System.out.println(list);            // [a, b, c, z]
System.out.println(rev);             // [z, c, b, a]

LinkedHashSet 的 addFirst 对已存在元素是移动而不是重复插入;List 的 addFirst 则允许重复,直接插入。这是 Set 与 List 的语义差异,不是 API 差异。

LinkedHashSet<String> set = new LinkedHashSet<>(List.of("a", "b", "c"));
set.addFirst("c");
System.out.println(set); // [c, a, b] —— 已存在元素被移到最前
reversed() 是视图:对视图的修改会写回原集合,反之亦然。它不是 Collections.reverse 那种原地反转,也不是拷贝。若原集合不可变,视图的修改方法会抛 UnsupportedOperationException。

SequencedMap:有序映射的统一入口04

SequencedMap 为有序 Map 提供首尾条目、首尾插入与反转视图。LinkedHashMap、TreeMap、SortedMap 都实现了它。

LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

System.out.println(map.firstEntry()); // a=1
System.out.println(map.lastEntry());  // c=3

map.putFirst("c", 30);                // 已存在的 key 移到最前,返回旧值 3
System.out.println(map);              // {c=30, a=1, b=2}

System.out.println(map.sequencedKeySet());   // [c, a, b]
System.out.println(map.sequencedValues());   // [30, 1, 2]
System.out.println(map.reversed());          // {b=2, a=1, c=30}

Map.Entry<String, Integer> first = map.pollFirstEntry(); // 取出并删除 c=30
System.out.println(first + " → " + map);     // c=30 → {a=1, b=2}

TreeMap 的第一/最后由排序决定,不是插入顺序。它支持 firstEntry / lastEntry / reversed,但 putFirst / putLast 会抛 UnsupportedOperationException——因为位置由比较器决定,不能随意插入。

TreeMap<String, Integer> tm = new TreeMap<>();
tm.put("b", 2);
tm.put("a", 1);
System.out.println(tm.firstEntry()); // a=1
System.out.println(tm.reversed());   // {b=2, a=1}
tm.putFirst("c", 3);                 // UnsupportedOperationException
  • 只读包装:Collections.unmodifiableSequencedCollection / unmodifiableSequencedSet / unmodifiableSequencedMap。
  • 从 Map 造 Set:Collections.newSequencedSetFromMap(map),适合需要「有序 Set 视图」的底层实现。
  • SequencedMap.reversed() 同样是视图,写入会反映到原 Map。

常见坑与真实行为05

1. subList 是视图

List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> sub = list.subList(1, 4); // [2, 3, 4]
sub.set(0, 20);
System.out.println(list); // [1, 20, 3, 4, 5] —— 原列表被修改

list.add(6);              // 结构修改后,sub 再访问会抛 ConcurrentModificationException

2. 遍历时删除

// 错误:ConcurrentModificationException
for (String s : list) {
    if (s.isBlank()) list.remove(s);
}

// 正确:Iterator.remove
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().isBlank()) it.remove();
}

// 或 removeIf
list.removeIf(String::isBlank);

3. computeIfAbsent 的返回值

Map<String, List<String>> index = new HashMap<>();
index.computeIfAbsent("a", k -> new ArrayList<>()).add("x");
System.out.println(index); // {a=[x]}

// 注意:mapping function 返回 null 时不会插入,也不会抛异常
index.computeIfAbsent("b", k -> null);
System.out.println(index.containsKey("b")); // false

4. equals / hashCode 可变 key

把可变对象放进 HashSet 或作为 HashMap 的 key,修改字段后哈希值变化,会导致再也找不到该元素。key 应该是不可变对象——record 是很好的选择。

5. 并发集合不是万能

ConcurrentHashMap 只保证单个操作的原子性。if (!map.containsKey(k)) map.put(k, v) 这种复合操作仍要使用 putIfAbsent / computeIfAbsent。

常用配方06

词频统计

Map<String, Long> freq = words.stream()
    .collect(Collectors.groupingBy(w -> w, Collectors.counting()));

Map<String, Integer> freq2 = new HashMap<>();
for (String w : words) {
    freq2.merge(w, 1, Integer::sum);
}

按 key 分组

Map<String, List<Order>> byUser = orders.stream()
    .collect(Collectors.groupingBy(Order::userId));

// 保留插入顺序
Map<String, List<Order>> ordered = orders.stream()
    .collect(Collectors.groupingBy(
        Order::userId, LinkedHashMap::new, Collectors.toList()));

LRU 缓存(LinkedHashMap)

class LruCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    LruCache(int capacity) {
        super(capacity, 0.75f, true); // accessOrder = true
        this.capacity = capacity;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

var cache = new LruCache<String, Integer>(3);
cache.put("a", 1); cache.put("b", 2); cache.put("c", 3);
cache.get("a");          // 访问 a,a 变成最近使用
cache.put("d", 4);       // 淘汰 b
System.out.println(cache.keySet()); // [c, a, d]
JDK 21 的 SequencedMap 让「固定容量保序缓存」可以更直接地实现,但 LinkedHashMap 的 accessOrder 语义仍需构造器参数开启,putFirst 不会自动改变访问顺序。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
List.of / Set.of / Map.of9 正式JDK 8 只能用 Collections.unmodifiableList 等包装。
SequencedCollection21 正式List / Deque / LinkedHashSet / SortedSet 均实现。
SequencedMap21 正式LinkedHashMap / TreeMap / SortedMap 实现;TreeMap 的 putFirst 不支持。
Collections.unmodifiableSequenced*21 正式分别对应 Collection / Set / Map 的只读包装。
Collections.newSequencedSetFromMap21 正式从有序 Map 构造 SequencedSet 视图。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. SequencedCollection 和 List 是什么关系?为什么已经有 List 还要新接口?
List 是索引访问的集合,SequencedCollection 抽的是「有确定顺序、支持首尾操作与反转视图」这一更窄的能力。Deque、LinkedHashSet、SortedSet 并不都是 List,但都需要首尾操作;统一接口后,API 可以面向能力编程,而不是面向实现类型。
02. 对 list.reversed() 调用 addFirst,原 list 会发生什么?
reversed() 是视图,添加会写回原列表的尾部。例如原 list 为 [a,b,c],rev.addFirst(z) 后原 list 变成 [a,b,c,z],rev 变为 [z,c,b,a]。它不是拷贝。
03. 为什么 TreeMap.putFirst 会抛 UnsupportedOperationException?
TreeMap 的顺序由 key 的比较器决定,不能任意把元素放到首尾;putFirst 与排序语义冲突,因此 Java 21 的实现选择抛出 UnsupportedOperationException。但它仍支持 firstEntry、lastEntry 与 reversed 视图。
04. Arrays.asList 与 List.of 的关键区别是什么?
Arrays.asList 是固定长度、可 set 的数组视图,修改会写回原数组;List.of 是真正不可变集合,拒绝 null 与重复元素,任何修改都抛 UnsupportedOperationException。
学习进度 记录保存在本机浏览器 · 不上传

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

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