JDK21 SequencedCollection:统一集合的有序操作接口
JDK21 SequencedCollection:统一集合的有序操作接口
适读人群:Java中高级开发者、日常使用集合框架的后端工程师 | 阅读时长:约12分钟 | 文章类型:特性详解+迁移建议
开篇故事
有一次我在写代码,需要取一个List的最后一个元素。
我知道list.get(list.size() - 1),但感觉很别扭——为什么没有list.getLast()?
然后我需要取一个LinkedHashSet的第一个元素,发现根本没有简单的API,只能iterator().next()。
这种小摩擦每次都让我分心。JDK21的SequencedCollection解决了这个问题。
一、为什么需要SequencedCollection
Java集合框架里,有很多"有序"的集合(元素有固定的首尾顺序),但它们的操作API非常不一致:
| 集合类 | 取第一个元素 | 取最后一个元素 | 倒序遍历 |
|---|---|---|---|
| ArrayList | get(0) | get(size-1) | 手动倒序 |
| LinkedList | getFirst() | getLast() | descendingIterator() |
| ArrayDeque | peekFirst() | peekLast() | 手动 |
| LinkedHashSet | iterator().next() | 没有简单方式 | 没有 |
| TreeSet | first() | last() | descendingSet() |
这乱得没法记。JDK21在集合框架里加了三个新接口,统一了这些操作。
二、三个新接口
三、核心原理深挖
接口位置在集合层次中的插入
JDK21把SequencedCollection插入到了现有的继承层次里,不破坏原有代码:
List extends SequencedCollectionDeque extends SequencedCollectionLinkedHashSet implements SequencedSet(SequencedSet extends SequencedCollection)SortedSet extends SequencedSet
这意味着原来的ArrayList、LinkedList、LinkedHashMap等,不需要修改代码,就自动拥有了getFirst()、getLast()等方法。
reversed()的实现
reversed()返回一个视图(view),不是新的集合:
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> reversed = list.reversed(); // 视图,不复制数据
// 对视图的读操作等价于倒序读原集合
System.out.println(reversed.get(0)); // 5(原集合的最后一个)
// 对视图的写操作会同步修改原集合
reversed.add(0, 99); // 在reversed的头部(即原集合的尾部)插入
System.out.println(list); // [1, 2, 3, 4, 5, 99]四、完整代码实现
代码一:新API的完整演示
package com.laozhang.jdk21;
import java.util.*;
/**
* JDK21 SequencedCollection/SequencedSet/SequencedMap 完整演示
*/
public class SequencedCollectionDemo {
public static void main(String[] args) {
System.out.println("=== List的新API ===");
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
// JDK21之前:list.get(0), list.get(list.size()-1)
// JDK21之后:
System.out.println("第一个: " + list.getFirst()); // a
System.out.println("最后一个: " + list.getLast()); // e
list.addFirst("Z"); // 在头部插入
System.out.println("addFirst后: " + list); // [Z, a, b, c, d, e]
list.addLast("X"); // 在尾部插入
System.out.println("addLast后: " + list); // [Z, a, b, c, d, e, X]
String first = list.removeFirst(); // 移除并返回第一个
String last = list.removeLast(); // 移除并返回最后一个
System.out.println("移除后: " + list + ", 移除了: " + first + ", " + last);
System.out.println("\n=== reversed()视图 ===");
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> rev = nums.reversed();
System.out.println("原始: " + nums); // [1, 2, 3, 4, 5]
System.out.println("倒序视图: " + rev); // [5, 4, 3, 2, 1]
// 用倒序视图遍历
System.out.print("倒序遍历: ");
for (int n : nums.reversed()) {
System.out.print(n + " ");
}
System.out.println();
System.out.println("\n=== LinkedHashSet(SequencedSet)===");
// LinkedHashSet是Set,之前没有简单的方式取首尾元素
SequencedSet<String> linkedSet = new LinkedHashSet<>(
List.of("apple", "banana", "cherry", "date"));
System.out.println("第一个: " + linkedSet.getFirst()); // apple
System.out.println("最后一个: " + linkedSet.getLast()); // date
System.out.println("倒序: " + linkedSet.reversed()); // [date, cherry, banana, apple]
System.out.println("\n=== LinkedHashMap(SequencedMap)===");
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
System.out.println("第一个entry: " + map.firstEntry()); // a=1
System.out.println("最后一个entry: " + map.lastEntry()); // c=3
Map.Entry<String, Integer> polled = map.pollFirstEntry(); // 移除并返回第一个
System.out.println("poll后map: " + map); // {b=2, c=3}
System.out.println("poll出来的: " + polled); // a=1
// 倒序遍历Map
System.out.println("倒序遍历: " + map.reversed());
System.out.println("\n=== 统一处理"任何有序集合"的代码 ===");
// 之前无法统一处理List和LinkedHashSet的首尾操作
// 现在可以:
processSequenced(new ArrayList<>(List.of("x", "y", "z")));
processSequenced(new LinkedHashSet<>(List.of("p", "q", "r")));
}
// 统一处理任何SequencedCollection
static <T> void processSequenced(SequencedCollection<T> col) {
System.out.printf("[%s] 第一个=%s, 最后一个=%s%n",
col.getClass().getSimpleName(),
col.getFirst(),
col.getLast());
}
}代码二:实际业务场景中的应用
package com.laozhang.jdk21;
import java.util.*;
import java.time.LocalDateTime;
/**
* SequencedCollection在业务代码中的实际应用
*/
public class SequencedCollectionBusiness {
record OperationLog(String operator, String action, LocalDateTime time) {}
// ===== 场景1:操作日志——取最新和最早的操作 =====
static class AuditService {
private final List<OperationLog> logs = new ArrayList<>();
public void record(String operator, String action) {
logs.add(new OperationLog(operator, action, LocalDateTime.now()));
}
// JDK21之前:logs.get(logs.size() - 1)
public OperationLog getLatest() { return logs.getLast(); }
// JDK21之前:logs.get(0)
public OperationLog getOldest() { return logs.getFirst(); }
// 取最近N条操作(倒序)
public List<OperationLog> getRecentLogs(int n) {
return logs.reversed().stream().limit(n).toList();
}
}
// ===== 场景2:LRU缓存(LinkedHashMap的有序特性)=====
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);
}
public V get(K key) {
return cache.getOrDefault(key, null);
}
public void put(K key, V value) {
if (cache.size() >= capacity) {
// JDK21:直接用pollFirstEntry()移除最旧的(最久未访问的)
cache.pollFirstEntry();
}
cache.put(key, value);
}
public String showOrder() {
return cache.sequencedKeySet().toString();
}
}
// ===== 场景3:处理有序的错误码集合 =====
static void analyzeErrors(SequencedSet<Integer> errorCodes) {
if (errorCodes.isEmpty()) {
System.out.println("无错误");
return;
}
System.out.println("错误码范围: " + errorCodes.getFirst() + " ~ " + errorCodes.getLast());
System.out.println("倒序(最新的先看): " + errorCodes.reversed());
}
public static void main(String[] args) throws Exception {
System.out.println("=== 操作日志 ===");
AuditService audit = new AuditService();
audit.record("张三", "创建订单");
Thread.sleep(10);
audit.record("李四", "审核通过");
Thread.sleep(10);
audit.record("王五", "发货");
System.out.println("最新操作: " + audit.getLatest().action());
System.out.println("最早操作: " + audit.getOldest().action());
System.out.println("最近2条: " + audit.getRecentLogs(2).stream()
.map(OperationLog::action).toList());
System.out.println("\n=== LRU缓存 ===");
LruCache<String, String> lru = new LruCache<>(3);
lru.put("a", "A");
lru.put("b", "B");
lru.put("c", "C");
System.out.println("插入abc后: " + lru.showOrder()); // [a, b, c]
lru.get("a"); // 访问a,a变为最近使用
System.out.println("访问a后: " + lru.showOrder()); // [b, c, a]
lru.put("d", "D"); // 插入d,淘汰最久未访问的b
System.out.println("插入d后: " + lru.showOrder()); // [c, a, d]
System.out.println("\n=== 错误码分析 ===");
SequencedSet<Integer> errors = new LinkedHashSet<>(List.of(400, 401, 403, 404, 500));
analyzeErrors(errors);
}
}四、踩坑实录
坑1:对空集合调用getFirst()/getLast()抛异常
报错现象:
java.util.NoSuchElementException根本原因:
getFirst()/getLast()在集合为空时抛出NoSuchElementException,不返回null。
具体解法:
// 先判空
if (!list.isEmpty()) {
System.out.println(list.getFirst());
}
// 或者用Optional包装
Optional.of(list)
.filter(l -> !l.isEmpty())
.map(List::getFirst)
.ifPresent(System.out::println);坑2:reversed()是视图,修改视图会影响原集合
触发代码:
List<Integer> original = new ArrayList<>(List.of(1, 2, 3));
List<Integer> reversed = original.reversed();
reversed.add(99); // 修改视图
System.out.println(original); // [1, 2, 3, 99],原集合被修改了!具体解法:
如果需要独立的倒序副本,不要用reversed(),改用:
List<Integer> copy = new ArrayList<>(original);
Collections.reverse(copy); // 或者用Stream坑3:JDK21以下版本没有这些API,代码无法编译
根本原因:
SequencedCollection是JDK21新增的接口,JDK17及以下没有。
具体解法:
升级到JDK21,或者保持旧写法。没有向前兼容的方案。如果项目要同时支持多个JDK版本,用条件编译或者工具类封装。
五、总结与延伸
SequencedCollection是一个小而美的改进。它不引入新的数据结构,只是给已有的有序集合加了统一的API。
一句话总结:以后取集合首尾元素,统一用getFirst()/getLast(),不用再记不同集合的不同写法了。
涉及的类:
ArrayList,LinkedList: 已实现SequencedCollectionArrayDeque: 已实现SequencedCollectionLinkedHashSet: 已实现SequencedSetTreeSet: 已实现SequencedSetLinkedHashMap: 已实现SequencedMapTreeMap: 已实现SequencedMap
如果你用JDK21,这是个零成本收益的特性——不需要学新概念,只是API更统一了。
