JDK21 Sequenced Collections:有序集合接口统一了哪些混乱
2026/4/30大约 10 分钟
JDK21 Sequenced Collections:有序集合接口统一了哪些混乱
适读人群:日常使用Java集合框架的后端开发者 | 阅读时长:约14分钟
开篇故事
2023年初,我做一次代码审查,看到一个同事写了这样的代码来获取LinkedHashMap的最后一个元素:
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
// ... 填充数据
// 获取最后一个entry
Map.Entry<String, Integer> last = null;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
last = entry; // 循环到最后一个
}遍历整个Map就为了拿最后一个元素,时间复杂度O(n)。我问他为什么不用直接的API,他说查了半天文档,LinkedHashMap没有getLast()这种方法。
确实。JDK21之前,这是个真实的痛点:
List有get(0)可以拿第一个,但没有统一的getFirst()Deque有peekFirst()/peekLast()LinkedHashMap/LinkedHashSet根本没有"第一个/最后一个"的API- 要反向遍历,每种集合的方式也不同
JDK21的Sequenced Collections用三个新接口一次性解决了这些混乱。
一、Sequenced Collections的背景
1.1 问题的根源
Java集合框架在1.2时代设计,那时候没有充分考虑"有序"这个维度:
原始集合层次(有问题的部分):
Collection
├── List(有顺序,有索引)
│ - 可以get(0), get(size-1)
│ - 没有统一的getFirst(), getLast()
│
└── Set
├── SortedSet(有顺序)
│ - first(), last()(方法名不同!)
└── LinkedHashSet(有顺序)
- 没有first(), last()!!
Map
├── SortedMap
│ - firstKey(), lastKey()(和SortedSet不一样的方法名!)
└── LinkedHashMap
- 没有firstEntry()等方法!!
Queue/Deque
- peekFirst(), peekLast()(又是不同的方法名!)这种不一致性让Java集合框架的"有序"概念非常混乱。
1.2 Sequenced Collections的解决方案
引入版本:JDK21(2023年9月,JEP 431)
三个新接口:
SequencedCollection<E> extends Collection<E>
SequencedSet<E> extends SequencedCollection<E>, Set<E>
SequencedMap<K,V> extends Map<K,V>
统一的API:
getFirst() / getLast()
addFirst() / addLast()
removeFirst() / removeLast()
reversed()(返回逆序视图)二、Sequenced Collections深度解析
2.1 新接口体系
SequencedCollection<E>:
E getFirst() — 第一个元素(空时抛NoSuchElementException)
E getLast() — 最后一个元素
void addFirst(E) — 在头部添加
void addLast(E) — 在尾部添加
E removeFirst() — 移除并返回第一个
E removeLast() — 移除并返回最后一个
SequencedCollection<E> reversed() — 返回逆序视图(不是拷贝!)
SequencedSet<E> extends SequencedCollection<E>, Set<E>:
SequencedSet<E> reversed() — 覆盖返回类型更具体
SequencedMap<K,V>:
Map.Entry<K,V> firstEntry() — 第一个entry
Map.Entry<K,V> lastEntry() — 最后一个entry
Map.Entry<K,V> pollFirstEntry() — 移除并返回第一个entry
Map.Entry<K,V> pollLastEntry() — 移除并返回最后一个entry
K firstKey() — 第一个key
K lastKey() — 最后一个key
V putFirst(K, V) — 在头部put
V putLast(K, V) — 在尾部put
SequencedMap<K,V> reversed() — 逆序视图
SequencedSet<K> sequencedKeySet() — 有序key集合
Collection<V> sequencedValues() — 有序value集合
SequencedSet<Map.Entry<K,V>> sequencedEntrySet() — 有序entry集合2.2 集合类型与新接口的关系
┌──────────────────────────────────────────────────────────┐
│ Sequenced Collections 继承体系 │
│ │
│ SequencedCollection │
│ ├── ArrayList (有序,可用getFirst/getLast) │
│ ├── LinkedList (有序,可用addFirst/addLast等) │
│ ├── ArrayDeque (Deque实现,可用getFirst/getLast) │
│ └── SequencedSet │
│ ├── LinkedHashSet (现在有getFirst/getLast了!) │
│ └── SortedSet/TreeSet (原有first()/last()已存在) │
│ │
│ SequencedMap │
│ ├── LinkedHashMap (现在有firstEntry/lastEntry了!) │
│ └── SortedMap/TreeMap (原有firstKey/lastKey已存在) │
└──────────────────────────────────────────────────────────┘Mermaid图:
2.3 reversed()是视图,不是拷贝
这是关键点:
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
List<String> reversed = list.reversed(); // 视图!不是新列表
// 修改原列表,reversed视图也会变化
list.add("d");
System.out.println(reversed); // [d, c, b, a] 同步变化!
// 修改reversed视图,原列表也会变化
reversed.addFirst("z"); // 在reversed的头部添加 = 在原list的尾部添加
System.out.println(list); // [a, b, c, d, z]三、完整代码示例
3.1 基础用法:旧写法vs新写法
import java.util.*;
import java.util.stream.*;
/**
* Sequenced Collections完整示例
* 引入版本:JDK21(2023年9月,JEP 431)
*/
public class SequencedCollectionsDemo {
// ===== 场景1:获取第一个/最后一个元素 =====
static void firstLastElements() {
// --- List ---
var list = new ArrayList<>(List.of("a", "b", "c", "d"));
// 旧写法
String firstOld = list.get(0);
String lastOld = list.get(list.size() - 1);
// 新写法
String first = list.getFirst(); // 干净!
String last = list.getLast();
System.out.println("First: " + first + ", Last: " + last);
// --- LinkedHashSet(以前完全没有API!)---
var set = new LinkedHashSet<>(List.of("apple", "banana", "cherry"));
// 旧写法:只能遍历
String firstSetOld = set.iterator().next();
// 获取最后一个需要遍历整个set或转成List
String lastSetOld = set.stream().reduce((a, b) -> b).orElse(null);
// 新写法
String firstSet = set.getFirst(); //
String lastSet = set.getLast();
System.out.println("Set first: " + firstSet + ", last: " + lastSet);
// --- LinkedHashMap ---
var map = new LinkedHashMap<String, Integer>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
// 旧写法:非常繁琐
Map.Entry<String, Integer> firstEntryOld = map.entrySet().iterator().next();
Map.Entry<String, Integer> lastEntryOld = null;
for (var e : map.entrySet()) lastEntryOld = e;
// 新写法
Map.Entry<String, Integer> firstEntry = map.firstEntry();
Map.Entry<String, Integer> lastEntry = map.lastEntry();
System.out.println("Map first: " + firstEntry + ", last: " + lastEntry);
}
// ===== 场景2:reversed()逆序视图 =====
static void reversedView() {
var list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
// 旧写法1:创建新列表(需要拷贝)
var reversedCopy = new ArrayList<>(list);
Collections.reverse(reversedCopy); // 修改原地
// 旧写法2:反向遍历
for (int i = list.size() - 1; i >= 0; i--) {
System.out.print(list.get(i) + " ");
}
System.out.println();
// 新写法:返回逆序视图(不拷贝!O(1)操作)
var reversedView = list.reversed();
System.out.println("Reversed: " + reversedView); // [5, 4, 3, 2, 1]
// 视图特性:原list变了,reversedView也变
list.add(6);
System.out.println("After add(6): " + reversedView); // [6, 5, 4, 3, 2, 1]
// LinkedHashMap逆序
var map = new LinkedHashMap<String, Integer>();
map.put("first", 1);
map.put("second", 2);
map.put("third", 3);
var reversedMap = map.reversed();
System.out.println("Map reversed: " + reversedMap.firstEntry()); // third=3
// Set逆序
var set = new LinkedHashSet<>(List.of("a", "b", "c"));
var reversedSet = set.reversed();
System.out.println("Set reversed: " + reversedSet); // [c, b, a]
}
// ===== 场景3:addFirst/addLast/removeFirst/removeLast =====
static void addRemoveFromEnds() {
// Deque一直有这些操作,现在List也有了
var list = new ArrayList<>(List.of("b", "c", "d"));
list.addFirst("a"); // 头部添加(注意:ArrayList头部添加是O(n)!)
list.addLast("e"); // 尾部添加(O(1))
System.out.println("After addFirst/addLast: " + list); // [a, b, c, d, e]
String removed = list.removeFirst();
System.out.println("removeFirst: " + removed + ", list: " + list); // a, [b, c, d, e]
// LinkedList的实现更高效(O(1)的头尾操作)
var deque = new LinkedList<>(List.of("b", "c", "d"));
deque.addFirst("a");
deque.addLast("e");
System.out.println("LinkedList: " + deque);
// LinkedHashMap的putFirst/putLast
var lhm = new LinkedHashMap<String, Integer>();
lhm.put("middle", 2);
lhm.putFirst("first", 1); // 插入到头部
lhm.putLast("last", 3); // 插入到尾部
System.out.println("LinkedHashMap ordered: " + lhm);
// {first=1, middle=2, last=3}
}
// ===== 场景4:sequencedKeySet/sequencedValues/sequencedEntrySet =====
static void sequencedMapViews() {
var map = new LinkedHashMap<String, Integer>();
map.put("banana", 2);
map.put("apple", 1);
map.put("cherry", 3);
// 旧写法:keySet()返回Set(非SequencedSet),没有getFirst等方法
// Set<String> keys = map.keySet(); // Set,无getFirst
// 新写法:
SequencedSet<String> keys = map.sequencedKeySet();
System.out.println("First key: " + keys.getFirst()); // banana(插入顺序)
System.out.println("Last key: " + keys.getLast()); // cherry
// 逆序遍历keys
for (String key : keys.reversed()) {
System.out.print(key + " ");
}
System.out.println();
// 有序entry集合
SequencedSet<Map.Entry<String, Integer>> entries = map.sequencedEntrySet();
System.out.println("First entry: " + entries.getFirst());
System.out.println("Last entry: " + entries.getLast());
}
public static void main(String[] args) {
System.out.println("=== 第一个/最后一个 ===");
firstLastElements();
System.out.println("\n=== 逆序视图 ===");
reversedView();
System.out.println("\n=== 头尾增删 ===");
addRemoveFromEnds();
System.out.println("\n=== Map有序视图 ===");
sequencedMapViews();
}
}3.2 实际应用场景
import java.util.*;
import java.time.*;
import java.util.stream.*;
/**
* Sequenced Collections在实际项目中的应用
*/
public class SequencedCollectionsInAction {
record LogEntry(Instant time, String level, String message) {}
record CacheEntry<V>(String key, V value, Instant insertedAt) {}
// ===== 场景1:LRU缓存实现(更简洁)=====
static class LRUCache<K, V> {
private final int capacity;
private final LinkedHashMap<K, V> cache;
LRUCache(int capacity) {
this.capacity = capacity;
// accessOrder=true:访问时移到末尾(最近访问的在末尾)
this.cache = new LinkedHashMap<>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
};
}
public V get(K key) {
return cache.getOrDefault(key, null); // 会自动移到末尾
}
public void put(K key, V value) {
cache.put(key, value);
}
public K getLRUKey() {
// 旧写法:繁琐
// return cache.keySet().iterator().next();
// 新写法:
return cache.sequencedKeySet().getFirst(); // 最久未访问的
}
public Map.Entry<K, V> getMRUEntry() {
return cache.lastEntry(); // 最近访问的
}
public K getMRUKey() {
return cache.sequencedKeySet().getLast(); // 最近访问的key
}
}
// ===== 场景2:操作历史(撤销/重做)=====
static class OperationHistory<T> {
private final LinkedList<T> history = new LinkedList<>();
private final int maxSize;
private int currentIndex = -1;
OperationHistory(int maxSize) {
this.maxSize = maxSize;
}
public void push(T operation) {
// 如果当前不在末尾(有redo历史),清除之后的
while (history.size() > currentIndex + 1) {
history.removeLast(); // 新写法
}
history.addLast(operation); // 新写法
currentIndex++;
// 超过maxSize,移除最老的
if (history.size() > maxSize) {
history.removeFirst(); // 新写法
currentIndex--;
}
}
public Optional<T> undo() {
if (currentIndex < 0) return Optional.empty();
return Optional.of(history.get(currentIndex--));
}
public Optional<T> redo() {
if (currentIndex >= history.size() - 1) return Optional.empty();
return Optional.of(history.get(++currentIndex));
}
public Optional<T> current() {
if (currentIndex < 0) return Optional.empty();
return Optional.of(history.get(currentIndex));
}
public List<T> getRecentHistory(int n) {
// 获取最近n个操作(新写法更清晰)
return history.reversed().stream()
.limit(n)
.collect(Collectors.toList());
}
}
// ===== 场景3:日志环形缓冲区 =====
static class LogBuffer {
private final LinkedList<LogEntry> buffer = new LinkedList<>();
private final int maxSize;
LogBuffer(int maxSize) {
this.maxSize = maxSize;
}
public void add(LogEntry entry) {
buffer.addLast(entry); // 新写法(比addFirst语义更清晰)
if (buffer.size() > maxSize) {
buffer.removeFirst(); // 移除最旧的
}
}
public LogEntry getLatest() {
return buffer.getLast(); // 新写法
}
public LogEntry getOldest() {
return buffer.getFirst(); // 新写法
}
public List<LogEntry> getLastN(int n) {
return buffer.reversed().stream()
.limit(n)
.collect(Collectors.toList());
}
public List<LogEntry> getErrorsInRecentOrder() {
return buffer.reversed().stream() // 新写法:直接逆序遍历
.filter(e -> "ERROR".equals(e.level()))
.collect(Collectors.toList());
}
}
public static void main(String[] args) {
System.out.println("=== LRU缓存 ===");
var cache = new LRUCache<String, String>(3);
cache.put("a", "Apple");
cache.put("b", "Banana");
cache.put("c", "Cherry");
cache.get("a"); // 访问a,a变成MRU
System.out.println("LRU key: " + cache.getLRUKey()); // b(最久未访问)
System.out.println("MRU key: " + cache.getMRUKey()); // a(最近访问)
System.out.println("\n=== 操作历史 ===");
var history = new OperationHistory<String>(5);
history.push("操作1");
history.push("操作2");
history.push("操作3");
System.out.println("当前: " + history.current()); // 操作3
System.out.println("撤销: " + history.undo()); // 操作3
System.out.println("当前: " + history.current()); // 操作2
System.out.println("重做: " + history.redo()); // 操作3
System.out.println("最近历史: " + history.getRecentHistory(2));
System.out.println("\n=== 日志缓冲区 ===");
var logBuffer = new LogBuffer(5);
logBuffer.add(new LogEntry(Instant.now(), "INFO", "启动"));
logBuffer.add(new LogEntry(Instant.now(), "ERROR", "连接失败"));
logBuffer.add(new LogEntry(Instant.now(), "INFO", "重连"));
logBuffer.add(new LogEntry(Instant.now(), "ERROR", "超时"));
logBuffer.add(new LogEntry(Instant.now(), "INFO", "恢复"));
System.out.println("最新日志: " + logBuffer.getLatest().message());
System.out.println("最旧日志: " + logBuffer.getOldest().message());
System.out.println("最近2条错误: " +
logBuffer.getErrorsInRecentOrder().stream()
.map(LogEntry::message)
.collect(Collectors.toList()));
}
}四、踩坑实录
坑1:ArrayList.addFirst()性能问题
// 警告:ArrayList.addFirst()是O(n)操作,不是O(1)
var list = new ArrayList<>(List.of(1, 2, 3));
list.addFirst(0); // 需要移动所有元素!O(n)
// 如果需要频繁的头部操作,用LinkedList或ArrayDeque
var deque = new ArrayDeque<>(List.of(1, 2, 3));
deque.addFirst(0); // O(1)
// 对于LinkedHashMap/LinkedHashSet,putFirst同样是O(n)
// 因为底层需要重新排列
var lhm = new LinkedHashMap<String, Integer>();
lhm.put("b", 2);
lhm.put("c", 3);
lhm.putFirst("a", 1); // 警告:可能是O(n)操作坑2:reversed()视图的修改影响原集合
// 容易混淆:reversed()不是深拷贝!
var original = new ArrayList<>(List.of("a", "b", "c"));
var view = original.reversed();
// 通过view修改,原集合也变
view.addFirst("z"); // 在view的头部 = 在original的尾部添加
System.out.println(original); // [a, b, c, z] -- 原集合变了!
// 如果需要不影响原集合的逆序:
var reversedCopy = new ArrayList<>(original.reversed()); // 拷贝
reversedCopy.addFirst("new"); // 只影响拷贝
System.out.println(original); // 不变坑3:getFirst/getLast对空集合抛异常
var emptyList = new ArrayList<String>();
try {
emptyList.getFirst(); // NoSuchElementException!(不是null)
} catch (NoSuchElementException e) {
System.out.println("Empty: " + e.getMessage());
}
// 安全获取:
Optional<String> first = emptyList.isEmpty() ? Optional.empty() : Optional.of(emptyList.getFirst());
// 或者用peek(Deque接口):
var deque = new ArrayDeque<String>();
String peeked = deque.peekFirst(); // 返回null而不是抛异常(Deque特有)坑4:SortedSet/TreeSet和SequencedSet的重叠API
// TreeSet同时实现了SortedSet和SequencedSet
// 它有两套"获取第一个/最后一个"的API
var treeSet = new TreeSet<>(List.of(3, 1, 4, 1, 5));
// SortedSet的旧API(基于自然顺序/Comparator排序)
Integer first1 = treeSet.first(); // 1
Integer last1 = treeSet.last(); // 5
// SequencedSet的新API(按迭代顺序,与first()/last()等价)
Integer first2 = treeSet.getFirst(); // 1
Integer last2 = treeSet.getLast(); // 5
// 对TreeSet,两者结果相同(迭代顺序就是排序顺序)
// 对LinkedHashSet,只有getFirst/getLast(没有first/last)
// 注意:不要混淆排序顺序和插入顺序坑5:sequencedEntrySet()返回的视图不支持所有操作
var map = new LinkedHashMap<String, Integer>();
map.put("a", 1);
map.put("b", 2);
SequencedSet<Map.Entry<String, Integer>> entries = map.sequencedEntrySet();
// 可以:读操作
System.out.println(entries.getFirst()); // a=1
System.out.println(entries.reversed().getFirst()); // b=2
// 注意:通过entries视图修改可能有限制
// 比如add操作在entry set上是不支持的(通过Map接口添加)
try {
entries.addFirst(Map.entry("c", 3)); // UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("不支持通过entrySet添加");
}
// 正确:直接通过map修改
map.putFirst("c", 3);五、总结与延伸
5.1 Sequenced Collections解决了什么
| 问题 | 以前 | 现在 |
|---|---|---|
| LinkedHashSet获取第一个 | 遍历或转换 | set.getFirst() |
| LinkedHashMap获取最后一个entry | 遍历整个Map | map.lastEntry() |
| 逆序遍历List | 手动for循环或Collections.reverse(拷贝) | list.reversed() |
| 不同集合类型统一处理"有序" | 每种类型API不同 | 统一的SequencedCollection接口 |
5.2 版本兼容建议
- JDK21(LTS):Sequenced Collections GA,可以立即使用
- 迁移建议:逐步将遍历型代码替换为getFirst/getLast,特别是LinkedHashMap/LinkedHashSet的场景
- 性能提醒:
reversed()是视图(O(1)),Collections.reverse()是原地反转(O(n)),new ArrayList<>(list.reversed())是拷贝(O(n))
