集合与 SequencedCollection
学完你要能自己回答
- SequencedCollection 解决的是什么历史问题?它和 List、Deque 是什么关系?
- reversed() 返回的是拷贝还是视图?对反转后的集合写入,原集合会变吗?
- putFirst 在 LinkedHashMap 和 TreeMap 上行为一样吗?为什么?
选型:先确定访问模式01
集合选型只需要回答四个问题:是否需要顺序、是否允许重复、是否需要按键查找、是否需要并发。大部分性能问题不是集合选错,而是用错访问模式(例如在 ArrayList 头部频繁插入)。
| 实现 | 底层结构 | 顺序 | 随机访问 | 典型场景 |
|---|---|---|---|---|
ArrayList | 动态数组 | 插入序 | O(1) | 最常用;尾部追加、遍历 |
LinkedList | 双向链表 | 插入序 | O(n) | 几乎不推荐;需要 Deque 时用 ArrayDeque |
ArrayDeque | 循环数组 | 插入序 | 仅首尾 | 栈、队列、双端队列首选 |
HashSet | 哈希表 | 无保证 | — | 去重、存在性判断 |
LinkedHashSet | 哈希表 + 链表 | 插入序 | — | 去重但保持顺序 |
TreeSet | 红黑树 | 排序序 | — | 需要有序或范围查询 |
HashMap | 哈希表 | 无保证 | — | 通用键值存储 |
LinkedHashMap | 哈希表 + 链表 | 插入/访问序 | — | LRU 缓存、保序输出 |
TreeMap | 红黑树 | 键排序 | — | 范围查询、有序遍历 |
ConcurrentHashMap | 分段/桶级并发 | 无保证 | — | 多线程共享映射 |
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]
putFirst 不会自动改变访问顺序。版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| List.of / Set.of / Map.of | 9 正式 | JDK 8 只能用 Collections.unmodifiableList 等包装。 |
| SequencedCollection | 21 正式 | List / Deque / LinkedHashSet / SortedSet 均实现。 |
| SequencedMap | 21 正式 | LinkedHashMap / TreeMap / SortedMap 实现;TreeMap 的 putFirst 不支持。 |
| Collections.unmodifiableSequenced* | 21 正式 | 分别对应 Collection / Set / Map 的只读包装。 |
| Collections.newSequencedSetFromMap | 21 正式 | 从有序 Map 构造 SequencedSet 视图。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。