系列:Java 后端实战精通营 · 作者:鱼宵
配套课程仓库:https://gitee.com/j67mk2/java21-journey (第 7 课)
先看一个尴尬的面试现场
面试官问:"取一个集合的第一个元素,怎么写?"
你心想这还不简单------然后大脑里同时蹦出三个答案:
- List 用
get(0) - ArrayDeque 用
peekFirst() - TreeSet 用
first()
同一个动作,三个窗口三套话术。面试当场你会愣一下:"我该用哪个?"。日常写代码更烦:换个集合类型就要换写法,代码里全是这种"方言"。
这还只是"取头","取尾"更乱:get(size-1)、peekLast()、last(),各写各的。
今天这篇,Java 21 的 SequencedCollection(顺序集合)把这堆方言一次全收:取头统一 getFirst(),取尾统一 getLast(),倒序统一 reversed(),List、Deque、Set、Map 全部通用。看完你可以在面试现场直接背出这个接口家族。
老代码到底乱在哪
我把痛点拆成两条,你就明白为什么需要新接口:
痛点一:取头取尾没有统一入口。 List 按下标(get(0)、get(size-1))、Deque 用 peekFirst/peekLast、SortedSet 用 first()/last()。三个接口三套 API,想写一个"对任意有序集合取末尾"的工具方法------以前根本找不到共同接口可以收。
痛点二:get(size-1) 是个隐藏的性能坑。 你以为"取末尾"就是 get(size-1),但在 LinkedList 上它是 O(n)------先算 size 再从头遍历到倒数第二个。你以为是统一写法,其实每种实现性能天差地别。
Java 21 的解法是 JEP 431(Sequenced Collections):定义一个新接口族,把"有序集合的正反序操作"统一成一套方法。
类比:食堂打饭队列。以前取队头,List 窗口说"报号"(get(0)),TreeSet 窗口说"拿第一个"(first()),ArrayDeque 窗口说"peek 一下"(peekFirst())------三个窗口三套话术。SequencedCollection 是食堂装了广播:"取队头 = getFirst,取队尾 = getLast",所有窗口统一口令。
接口家族:三件套
SequencedCollection<E> ← 根接口:getFirst/getLast/addFirst/addLast/removeFirst/removeLast/reversed
├── SequencedSet<E> ← Set 版,reversed() 返回的还是一个 Set
└── SequencedMap<K,V> ← Map 版:firstEntry()/lastEntry()/sequencedEntrySet()/reversed()
- SequencedSet:给 Set 用。LinkedHashSet(保持插入顺序)、TreeSet(保持排序顺序)都实现了它------顺序语义不同,但取头取尾的方法名一样。
- SequencedMap :给 Map 用。注意取头尾是
firstEntry()/lastEntry(),返回的是一个Map.Entry,不是firstKey()------那是 NavigableMap 时代的老方法,我写 demo 时就记错过(见踩坑节)。
关键点:老接口没被废,是"向上合并"。SortedSet / NavigableSet 在 JDK 21 已经改成 extends SequencedSet,TreeSet 现在是"老 SortedSet + 新 SequencedSet"双料。老代码不用急着改,只是有了统一的新名字。
完整可运行代码(复制就能跑)
本课就一个文件 Main.java,我贴全量,import 也齐:
java
package com.java21.lesson07;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedCollection;
import java.util.SequencedSet;
import java.util.TreeSet;
/**
* Java 21 实战精通营 · 第 7 课:顺序集合 SequencedCollection
*/
public class Main {
public static void main(String[] args) {
System.out.println("========== 第 7 课:顺序集合 SequencedCollection ==========");
// ---------- 1. List 统一正反序 ----------
System.out.println();
System.out.println("--- 1) List:getFirst / getLast / reversed ---");
SequencedCollection<String> list = new ArrayList<>(List.of("A", "B", "C"));
System.out.println("原集合:" + list);
System.out.println("第一个 getFirst:" + list.getFirst());
System.out.println("最后一个 getLast:" + list.getLast());
System.out.println("倒序视图 reversed:" + list.reversed());
// ---------- 2. Deque:同一个接口,同样的方法 ----------
System.out.println();
System.out.println("--- 2) Deque:getFirst / getLast 拿来就用 ---");
Deque<String> queue = new ArrayDeque<>(List.of("1号", "2号", "3号"));
System.out.println("原队列:" + queue);
System.out.println("队头 getFirst:" + queue.getFirst());
System.out.println("队尾 getLast:" + queue.getLast());
// ---------- 3. SequencedSet:插入序 vs 排序序 ----------
System.out.println();
System.out.println("--- 3) SequencedSet:LinkedHashSet 与 TreeSet ---");
SequencedSet<String> linkedHash = new LinkedHashSet<>(List.of("一", "二", "三"));
System.out.println("LinkedHashSet(保持插入序):" + linkedHash);
System.out.println("getFirst:" + linkedHash.getFirst() + ",getLast:" + linkedHash.getLast());
SequencedSet<String> tree = new TreeSet<>(List.of("b", "a", "c"));
System.out.println("TreeSet(自动排好序):" + tree);
System.out.println("getFirst:" + tree.getFirst() + ",getLast:" + tree.getLast());
// ---------- 4. LinkedHashMap:SequencedMap ----------
System.out.println();
System.out.println("--- 4) LinkedHashMap:SequencedMap ---");
LinkedHashMap<String, Integer> cart = new LinkedHashMap<>();
cart.put("苹果", 5);
cart.put("香蕉", 3);
cart.put("橙子", 8);
System.out.println("购物车(保持放入顺序):" + cart);
System.out.println("第一件 firstEntry:" + cart.firstEntry());
System.out.println("最后一件 lastEntry:" + cart.lastEntry());
System.out.println("sequencedEntrySet().getFirst():" + cart.sequencedEntrySet().getFirst());
// ---------- 5. 核心:reversed() 是视图 ----------
System.out.println();
System.out.println("--- 5) reversed() 是视图:改原集合,视图同步变 ---");
System.out.println("原集合 list:" + list);
SequencedCollection<String> rev = list.reversed();
System.out.println("倒序视图 rev:" + rev);
list.add("D");
System.out.println("list.add(\"D\") 之后------");
System.out.println("原集合 list:" + list);
System.out.println("倒序视图 rev(自动跟着变!):" + rev);
rev.addFirst("Z"); // 视图的"开头" = 原集合的"末尾"
System.out.println("rev.addFirst(\"Z\") 之后------");
System.out.println("倒序视图 rev:" + rev);
System.out.println("原集合 list(同步变!):" + list);
// ---------- 6. 老 API 对比 ----------
System.out.println();
System.out.println("--- 6) 老 API 对比:以前怎么取头尾 ---");
List<String> oldList = (List<String>) list;
System.out.println("List 老写法 get(0):" + oldList.get(0));
System.out.println("List 老写法 get(size-1):" + oldList.get(oldList.size() - 1));
TreeSet<String> oldTree = (TreeSet<String>) tree;
System.out.println("TreeSet 老写法 first():" + oldTree.first());
System.out.println("TreeSet 老写法 last():" + oldTree.last());
}
}
运行方式(JDK 21,本课不需要 --enable-preview,JEP 431 已转正):
bash
export JAVA_HOME=$HOME/tools/jdk-21.0.12.1+1
export PATH=$JAVA_HOME/bin:$PATH
java -version # 确认是 21,不是就换你自己的 JDK 路径
cd java21-journey/lesson-07
mvn -q clean install -DskipTests
java -cp target/classes com.java21.lesson07.Main
实测输出(本机 JDK 21.0.12.1)
text
========== 第 7 课:顺序集合 SequencedCollection ==========
--- 1) List:getFirst / getLast / reversed ---
原集合:[A, B, C]
第一个 getFirst:A
最后一个 getLast:C
倒序视图 reversed:[C, B, A]
--- 2) Deque:getFirst / getLast 拿来就用 ---
原队列:[1号, 2号, 3号]
队头 getFirst:1号
队尾 getLast:3号
--- 3) SequencedSet:LinkedHashSet 与 TreeSet ---
LinkedHashSet(保持插入序):[一, 二, 三]
getFirst:一,getLast:三
TreeSet(自动排好序):[a, b, c]
getFirst:a,getLast:c
--- 4) LinkedHashMap:SequencedMap ---
购物车(保持放入顺序):{苹果=5, 香蕉=3, 橙子=8}
第一件 firstEntry:苹果=5
最后一件 lastEntry:橙子=8
sequencedEntrySet().getFirst():苹果=5
--- 5) reversed() 是视图:改原集合,视图同步变 ---
原集合 list:[A, B, C]
倒序视图 rev:[C, B, A]
list.add("D") 之后------
原集合 list:[A, B, C, D]
倒序视图 rev(自动跟着变!):[D, C, B, A]
rev.addFirst("Z") 之后------
倒序视图 rev:[Z, D, C, B, A]
原集合 list(同步变!):[A, B, C, D, Z]
--- 6) 老 API 对比:以前怎么取头尾 ---
List 老写法 get(0):A
List 老写法 get(size-1):Z
TreeSet 老写法 first():a
TreeSet 老写法 last():c
第 5 段是本课最值钱的证据,我再说人话解释一遍:
list.add("D")之后,rev 从[C,B,A]变成[D,C,B,A]------视图跟着原集合变(像照镜子,你剪头发镜子里的人也跟着剪)rev.addFirst("Z")之后,原集合变成[A,B,C,D,Z]------通过视图写,写穿到原集合。因为视图的"开头"就是原集合的"末尾"。
两个方向都验证了:reversed() 返回的不是新列表,是同一个数据的倒序窗口。想拿它当快照?不行,原集合一改它就变。要独立副本就 new ArrayList<>(coll.reversed()) 自己拷一份。
挑战题(答案都在源码里)
- ⭐ 换实现 :把第 2 节的
ArrayDeque换成LinkedList,getFirst/getLast输出是不是一样?体会"接口统一"。 - ⭐⭐ 证明写穿 :在第 5 节后面补一行
rev.removeFirst(),打印原集合------观察原集合末尾元素被删掉(提示:视图头部 = 原集合尾部)。 - ⭐⭐⭐ 面试加分 :写一个工具方法
static <E> E peekTail(SequencedCollection<E> c),任意有序集合取最后一个元素;分别传 TreeSet 和 ArrayDeque 测试。然后想:这个方法在 JDK 21 之前为什么写不出来? (答案:以前没有一个共同接口同时覆盖 List/Deque/SortedSet 的正反序操作,get(size-1)和first()收不到一个方法里。)
踩坑:我自己写 demo 时翻过的车
firstKey()编译不过 :SequencedMap 取头尾是firstEntry()/lastEntry(),返回Map.Entry。firstKey()是 NavigableMap 的老方法------Map 侧和 Collection 侧的头尾 API 长得不一样,别套。- 以为 reversed() 是拷贝 :它是视图!我一开始把
rev当快照存着,原集合一改,rev 悄悄变了,排查了半天。要快照就new ArrayList<>(...)包一层。 - 空集合调 getFirst() :直接抛
NoSuchElementException(老get(0)抛IndexOutOfBoundsException,现在统一了)。怕空就用pollFirst(),空了返回 null。 - 对视图按下标循环 :
reversed()视图创建是 O(1),但视图上按逻辑下标取元素要从头走。别写for (int i = 0; i < rev.size(); i++),用增强 for / 迭代器。
面试回答模板
面试官问:"reversed() 返回的是新集合吗?"
一句话结论:不是,它是原集合的一个反向视图,没有数据拷贝。
展开补两句原理:我实测过------原集合末尾加一个元素,倒序视图立刻在最前面多出这个元素;反过来往视图头部加元素,等于往原集合尾部加。两边共享同一份数据,只是遍历方向相反。
收一句应用场景:所以拿 reversed() 当临时倒序遍历很好用(O(1) 创建),但别把它当快照存起来------原集合一改它就跟着变;需要独立副本就 new ArrayList<> 包一层。
顺带把家族名背熟:SequencedCollection / SequencedSet / SequencedMap,关键词 JEP 431、统一正反序、reversed 是视图。
总结表
| 痛点 | 老写法(各写各的) | Java 21 统一写法 |
|---|---|---|
| 取第一个 | List get(0) / Deque peekFirst() / Set first() |
一律 getFirst() |
| 取最后一个 | List get(size-1) / Deque peekLast() / Set last() |
一律 getLast() |
| 倒序遍历 | descendingIterator() / descendingSet() 等方言 |
一律 reversed()(视图) |
| Map 取头尾 | 没有统一办法 | firstEntry() / lastEntry() |
| 面试关键词 | ------ | SequencedCollection / JEP 431 / 视图 |
下篇预告
下节课讲字符串模板 :告别"+"号和 StringBuilder,模板里直接插变量(预览特性)。到时候你会发现:拼字符串这个写了几万次的活,Java 终于给了个像样的写法。
源码都在:https://gitee.com/j67mk2/java21-journey 的 lesson-07/ 目录,clone 下来 mvn clean install 就能跑。
评论区聊聊:你以前取集合头尾都用的哪套写法?跑出来输出和我截图不一样的话,把报错贴出来一起查。