1. 经典集合中的顺序问题
在 Java 中一直存在保证元素顺序的集合(例如 ArrayList、LinkedList、LinkedHashSet、LinkedHashMap),以及不保证顺序的集合(例如 HashSet、HashMap)。但这些集合有一个共同缺点:即使其中一些以特定顺序存储元素,标准接口(List、Set、Map)也没有提供通用的方法来获取第一个/最后一个元素,或将顺序反转。
例如,对于 List<String>,你可以通过 list.get(0) 拿到第一个元素,但对 Set 或 Map 就不行了——不得不使用迭代器或写额外样板代码。这既不方便,也不利于可读性。
这就好比你有一个带抽屉的柜子,但每次要拿第一个或最后一个抽屉时,都得把所有抽屉重新数一遍!要是有“第一个”和“最后一个”的专用把手就好了,对吧?
SequencedCollection 及相关接口的出现
在 Java 21 中新增了如下集合接口:
- SequencedCollection<E>
- SequencedSet<E>
- SequencedMap<K, V>
这些接口在标准集合之上扩展,提供了统一的有序操作方式。现在可以针对任何重视顺序的集合编写通用代码,而无需关心具体实现。
这是什么?
- SequencedCollection:元素具有明确顺序的集合,你可以轻松获取第一个、最后一个元素,并可反转顺序。
- SequencedSet:同上,但作用于集合(元素唯一)。
- SequencedMap:同上,但作用于映射(键-值)。
哪些集合现在实现了它们?
在 Java 21 中,以下标准集合实现了这些新接口:
- ArrayList、LinkedList → SequencedCollection
- LinkedHashSet、TreeSet → SequencedSet
- LinkedHashMap、TreeMap → SequencedMap
这意味着如果你已经在使用这些集合,就会自动获得这些新能力!
2. SequencedCollection、SequencedSet、SequencedMap 的核心方法
方法 SequencedCollection
E getFirst(); // 获取第一个元素
E getLast(); // 获取最后一个元素
SequencedCollection<E> reversed(); // 获取逆序的集合视图
方法 SequencedSet
与 SequencedCollection 相同的方法,外加 Set 的全部能力。
方法 SequencedMap
Map.Entry<K, V> firstEntry(); // 第一个条目(键-值)
Map.Entry<K, V> lastEntry(); // 最后一个条目(键-值)
SequencedMap<K, V> reversed(); // 逆序的 Map 视图
3. 新接口的使用示例
示例 1:获取第一个和最后一个元素
import java.util.*;
public class SequencedDemo {
public static void main(String[] args) {
SequencedCollection<String> sc = new ArrayList<>();
sc.add("Java");
sc.add("Python");
sc.add("Kotlin");
// 获取第一个和最后一个元素
String first = sc.getFirst(); // "Java"
String last = sc.getLast(); // "Kotlin"
System.out.println("第一个:" + first);
System.out.println("最后一个:" + last);
}
}
输出:
第一个:Java
最后一个:Kotlin
示例 2:反转集合
SequencedCollection<String> sc = new LinkedList<>();
sc.add("A");
sc.add("B");
sc.add("C");
SequencedCollection<String> reversed = sc.reversed();
System.out.println(reversed); // [C, B, A]
请注意:reversed() 返回的是集合的逆序视图。如果修改原集合,reversed 视图也会随之改变!
示例 3:使用 SequencedSet
SequencedSet<Integer> set = new LinkedHashSet<>();
set.add(100);
set.add(200);
set.add(300);
System.out.println("第一个元素:" + set.getFirst()); // 100
System.out.println("最后一个元素:" + set.getLast()); // 300
SequencedSet<Integer> reversedSet = set.reversed();
System.out.println(reversedSet); // [300, 200, 100]
示例 4:使用 SequencedMap
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("apple", 5);
map.put("banana", 3);
map.put("cherry", 7);
Map.Entry<String, Integer> first = map.firstEntry();
Map.Entry<String, Integer> last = map.lastEntry();
System.out.println("第一个:" + first.getKey() + " = " + first.getValue()); // apple = 5
System.out.println("最后一个:" + last.getKey() + " = " + last.getValue()); // cherry = 7
SequencedMap<String, Integer> reversedMap = map.reversed();
System.out.println(reversedMap); // {cherry=7, banana=3, apple=5}
4. 这与您的应用有什么关系?
假设在你的教学应用中,你维护了一个已登录用户列表,希望能快速拿到第一个和最后一个用户(例如用于展示“谁最先登录”和“谁最后登录”)。以前你可能会写成:
List<String> users = new ArrayList<>();
// ... 添加用户
String first = users.get(0);
String last = users.get(users.size() - 1);
但如果集合不是列表,而是 LinkedHashSet(元素唯一且保持插入顺序),上述技巧就不适用了:
Set<String> users = new LinkedHashSet<>();
// ... 添加用户
// 如何获取第一个?只能通过迭代器:
String first = users.iterator().next();
// 那最后一个呢?只能遍历所有元素!
现在更简单且更通用:
SequencedSet<String> users = new LinkedHashSet<>();
// ... 添加用户
String first = users.getFirst();
String last = users.getLast();
这不仅减少了代码量,还让代码更易读、更安全。
5. 新接口的关系图
classDiagram
Collection <|-- SequencedCollection
List <|-- SequencedCollection
Set <|-- SequencedSet
Map <|-- SequencedMap
SequencedCollection <|-- SequencedSet
SequencedSet <|-- LinkedHashSet
SequencedSet <|-- TreeSet
SequencedCollection <|-- ArrayList
SequencedCollection <|-- LinkedList
SequencedMap <|-- LinkedHashMap
SequencedMap <|-- TreeMap
6. 实用细节
SequencedCollection 的实际优势
- 统一的顺序操作接口:不必再记住哪里能用 get(0)、哪里要迭代器、哪里根本拿不到第一个元素。
- 处理队列和栈更方便:可以轻松获取第一个(head)和最后一个(tail)元素。
- 安全性与可读性:减少因误用集合导致的错误;代码更具自文档性。
- 快速反转集合:reversed() 让你无需手工操作即可得到逆序视图。
- 易于维护与扩展代码:未来若想把 ArrayList 替换为 LinkedHashSet,使用 SequencedCollection 的代码无需重写。
实现要点
- 并非所有集合都实现 SequencedCollection:例如 HashSet 和 HashMap 不保证顺序,因此不会实现这些新接口。
- 方法可能抛出异常:当集合为空时调用 getFirst() 或 getLast() 会抛出 NoSuchElementException。别忘了先检查集合是否为空!
- reversed() 是视图而不是副本:对原集合的修改会反映到逆序视图中,反之亦然。
- 兼容性:这些新接口仅从 Java 21 开始可用。如果你使用更低版本的 Java,暂时无法使用(但这正是升级的好理由!)。
- 泛型:所有新接口都完全支持泛型,因此可处理任意数据类型。
7. 使用 SequencedCollection 的常见错误
错误 1:对不保证顺序的集合期待有序行为。 如果你尝试将 HashSet 强制转换为 SequencedSet,会得到编译错误——HashSet 没有顺序,也没有实现该接口。
错误 2:忽略空集合。 在空集合上调用 getFirst() 或 getLast() 会抛出异常。在调用这些方法之前,请先检查集合是否为空:
if (!sc.isEmpty()) {
String first = sc.getFirst();
}
错误 3:对 reversed() 的误解。 reversed() 返回的是视图,而不是副本。如果你修改了 reversed 视图,原集合也会随之改变(反之亦然)。如果没有预期这种行为,可能会得到出乎意料的结果。
错误 4:在旧版 Java 上使用新接口。 如果你的项目使用的 Java 版本低于 Java 21,编译器将找不到这些接口。请在项目设置中检查 JDK 版本!
GO TO FULL VERSION