摘要: 本文深入讲解Java集合框架的实用技巧与最新特性,涵盖Collections工具类的排序、查找、不可变集合等核心功能,JDK 9+的集合工厂方法,JDK 8+的高级操作如removeIf()、computeIfAbsent(),以及JDK 21的Sequenced Collections统一接口。通过性能优化建议和综合示例,帮助开发者编写更简洁、高效、安全的集合代码。
关键词: Java集合, Collections工具类, JDK新特性, 不可变集合, 性能优化
经过前两篇的学习,你已经掌握了 Java 集合框架的三大核心家族:List、Set、Map 和 Queue。你知道什么时候用 ArrayList,什么时候用 HashMap,也知道了 HashSet 能自动去重,TreeMap 能按键排序。
但如果你只停留在“会用基本方法”的层面,那就太可惜了。集合框架就像一个巨大的工具箱——你不仅要知道扳手和螺丝刀是干什么的,还要知道它们有哪些隐藏技巧:怎么快速排序?怎么创建不可变集合?怎么优雅地处理“如果键不存在就计算一个新值”?以及 JDK 21 又给这个工具箱增加了哪些新工具。
今天,我们就来把这些实用技巧和最新特性一网打尽。
1. Collections 工具类 —— 集合的“瑞士军刀”
Java 提供了一个专门操作集合的工具类 java.util.Collections(注意结尾有个 s,不是 Collection 接口)。它里面全是静态方法,几乎覆盖了你能想到的所有集合操作。
1.1 排序与反转
import java.util.*;
public class CollectionsSortDemo {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(2);
numbers.add(8);
numbers.add(1);
// 排序(升序)
Collections.sort(numbers);
System.out.println("升序:" + numbers); // [1, 2, 5, 8]
// 降序(先升序,再反转)
Collections.sort(numbers, Collections.reverseOrder());
System.out.println("降序:" + numbers); // [8, 5, 2, 1]
// 反转顺序(不排序,只反转当前顺序)
Collections.reverse(numbers);
System.out.println("反转:" + numbers); // [1, 2, 5, 8]
// 随机打乱
Collections.shuffle(numbers);
System.out.println("随机打乱:" + numbers);
}
}
1.2 查找与最值
List<Integer> list = Arrays.asList(3, 7, 1, 9, 4);
// 二分查找(必须先排序)
Collections.sort(list);
int index = Collections.binarySearch(list, 7);
System.out.println("7 的索引:" + index); // 3
// 最大值、最小值
System.out.println("最大值:" + Collections.max(list)); // 9
System.out.println("最小值:" + Collections.min(list)); // 1
1.3 不可变集合(只读视图)
有时候,你需要把一个集合传递给外部代码,但不希望外部代码修改它。Collections 提供了 unmodifiableXXX 方法,返回一个只读的包装集合。
List<String> original = new ArrayList<>();
original.add("A");
original.add("B");
List<String> readonly = Collections.unmodifiableList(original);
// readonly.add("C"); // 运行时抛出 UnsupportedOperationException
// 但 original 仍然可以修改,只读集合只是“视图”
original.add("C");
System.out.println(readonly); // [A, B, C] —— 能看到外部修改
如果你想创建一个真正不可变的集合(连原集合都不能修改),请使用后面的
List.of()工厂方法。
1.4 线程安全的包装
Collections 也提供了将非线程安全的集合包装成线程安全集合的方法,但不推荐使用(因为性能差且容易出错)。并发场景下,请使用 java.util.concurrent 包中的专用集合(如 ConcurrentHashMap、CopyOnWriteArrayList)。
// 不推荐(老式做法)
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
我们会在第五阶段专门讲并发集合,现在你只需要知道它们存在。
1.5 空集合与单元素集合
// 返回一个空的不可变 List
List<String> emptyList = Collections.emptyList();
// 返回只包含一个元素的不可变 List
List<String> singleton = Collections.singletonList("only one");
这些方法在需要返回空集合而不是 null 时非常有用,能避免调用方的空指针异常。
2. 集合工厂方法(JDK 9+)—— 一行代码创建集合
在 Java 9 之前,创建一个包含初始元素的集合需要好几行:
// 老方式
List<String> list = new ArrayList<>();
list.add("苹果");
list.add("香蕉");
list.add("橘子");
从 Java 9 开始,List、Set、Map 接口都提供了静态工厂方法,让你用一行代码创建不可变的集合。
2.1 List.of()
// 创建不可变的 List
List<String> fruits = List.of("苹果", "香蕉", "橘子");
// fruits.add("葡萄"); // 运行时抛出 UnsupportedOperationException
2.2 Set.of()
// 创建不可变的 Set(自动去重)
Set<Integer> numbers = Set.of(1, 2, 3, 4, 5);
// 如果有重复元素,会抛出 IllegalArgumentException
// Set<Integer> invalid = Set.of(1, 2, 2); // 错误!
2.3 Map.of() 和 Map.ofEntries()
对于 Map,of() 方法支持最多 10 个键值对(交替传入键和值):
Map<String, Integer> scores = Map.of("张三", 85, "李四", 92, "王五", 78);
超过 10 个键值对时,用 Map.ofEntries():
Map<String, Integer> scores2 = Map.ofEntries(
Map.entry("张三", 85),
Map.entry("李四", 92),
Map.entry("王五", 78),
Map.entry("赵六", 88)
);
2.4 List.copyOf()、Set.copyOf()、Map.copyOf()
从已有集合创建不可变副本:
List<String> original = new ArrayList<>();
original.add("A");
original.add("B");
List<String> copy = List.copyOf(original); // 不可变副本
// 修改 original 不会影响 copy
original.add("C");
System.out.println(copy); // [A, B] 不受影响
使用建议:在不需要修改集合时,优先使用 List.of()、Set.of() 等工厂方法创建不可变集合。它们更安全、更简洁,而且在某些情况下还能节省内存。
3. 集合的高级操作(JDK 8+)
从 Java 8 开始,集合接口增加了几个非常实用的默认方法,让集合操作更加函数式、更流畅。
3.1 removeIf() —— 条件删除
删除集合中满足某个条件的元素,无需手动用迭代器。
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
// 删除所有偶数
numbers.removeIf(n -> n % 2 == 0);
System.out.println(numbers); // [1, 3, 5, 7, 9]
3.2 replaceAll() —— 批量替换(仅 List)
对 List 中的每个元素应用一个函数,替换原值。
List<String> names = new ArrayList<>(List.of("张三", "李四", "王五"));
// 每个名字前加上 "同学"
names.replaceAll(name -> name + "同学");
System.out.println(names); // [张三同学, 李四同学, 王五同学]
3.3 forEach() —— 遍历
Collection 接口从 JDK 8 开始有了 forEach() 方法,接收一个 Consumer(消费每个元素)。
List<String> items = List.of("A", "B", "C");
items.forEach(item -> System.out.println("元素:" + item));
// 或使用方法引用
items.forEach(System.out::println);
3.4 Map 的高级方法
Map 接口在 Java 8 中增加了一批非常实用的方法,让你避免写很多 if-else。
① computeIfAbsent() —— 如果键不存在,则计算新值并放入
Map<String, List<String>> map = new HashMap<>();
// 传统写法
List<String> list = map.get("fruits");
if (list == null) {
list = new ArrayList<>();
map.put("fruits", list);
}
list.add("苹果");
// 用 computeIfAbsent 一行搞定
map.computeIfAbsent("fruits", key -> new ArrayList<>()).add("香蕉");
map.computeIfAbsent("fruits", key -> new ArrayList<>()).add("橘子");
System.out.println(map.get("fruits")); // [香蕉, 橘子]
这在处理“分组”或“缓存”时特别好用。
② computeIfPresent() —— 如果键存在,则更新值
Map<String, Integer> scores = new HashMap<>(Map.of("张三", 85));
// 如果张三存在,成绩加 10
scores.computeIfPresent("张三", (key, val) -> val + 10);
System.out.println(scores.get("张三")); // 95
// 如果李四不存在,什么也不做
scores.computeIfPresent("李四", (key, val) -> val + 10); // 无影响
③ merge() —— 合并值,处理键冲突
当你想把新值与已存在的值合并时,merge() 非常方便。
Map<String, Integer> wordCount = new HashMap<>();
String sentence = "apple banana apple orange banana apple";
for (String word : sentence.split(" ")) {
// 如果 word 不存在,设为 1;如果存在,原值 + 1
wordCount.merge(word, 1, Integer::sum);
}
System.out.println(wordCount); // {banana=2, orange=1, apple=3}
④ getOrDefault() —— 带默认值的获取
Map<String, Integer> scores = new HashMap<>(Map.of("张三", 85));
int score = scores.getOrDefault("李四", 0);
System.out.println(score); // 0(李四不存在,返回默认值)
⑤ putIfAbsent() —— 仅当键不存在时才放入
Map<String, String> map = new HashMap<>();
map.put("key", "old");
map.putIfAbsent("key", "new"); // 因为 key 已存在,不会覆盖
System.out.println(map.get("key")); // old
这些方法让 Map 操作更加简洁、安全,避免了繁琐的空值检查。
4. JDK 21 的 Sequenced Collections —— 有序集合的统一
我们在前两篇已经简单提到过,现在来深入讲解。
在 JDK 21 之前,List 有 get(0) 和 get(size()-1) 来获取首尾,但 Set 没有统一的方式(LinkedHashSet 虽然保持插入顺序,但没有获取首尾的方法),Deque 有 getFirst() 和 getLast(),但接口不一致。
JDK 21 引入了一组新接口,统一了“有序集合”的操作:
SequencedCollection:List、Deque以及有序的Set(如LinkedHashSet)都实现了它。SequencedSet:SequencedCollection的子接口,专门给有序Set用的。SequencedMap:有序Map(如LinkedHashMap、TreeMap)实现的接口。
4.1 SequencedCollection 的核心方法
// 假设用一个有序集合
SequencedCollection<String> coll = new LinkedHashSet<>();
coll.add("A");
coll.add("B");
coll.add("C");
// 获取第一个和最后一个元素
String first = coll.getFirst(); // A
String last = coll.getLast(); // C
// 移除并返回第一个和最后一个元素
String removedFirst = coll.removeFirst(); // A
String removedLast = coll.removeLast(); // C
// 反转顺序,返回一个新的逆序视图
SequencedCollection<String> reversed = coll.reversed();
for (String s : reversed) {
System.out.print(s + " "); // 如果原始是 [B],反转后还是 [B]
}
对于 List,getFirst() 等价于 get(0),getLast() 等价于 get(size()-1),但更简洁。
4.2 SequencedSet 的特殊性
SequencedSet 多了一个 reversed() 方法,返回一个逆序的 SequencedSet。因为 Set 不允许重复,所以反转后也不会产生重复。
SequencedSet<String> set = new LinkedHashSet<>();
set.add("甲");
set.add("乙");
set.add("丙");
SequencedSet<String> reversedSet = set.reversed();
System.out.println(reversedSet); // [丙, 乙, 甲] —— 视图,不是新对象
4.3 SequencedMap 的核心方法
SequencedMap 提供了类似的操作来访问首尾键值对:
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
// 第一个和最后一个键值对
Map.Entry<String, Integer> first = map.firstEntry(); // one=1
Map.Entry<String, Integer> last = map.lastEntry(); // three=3
// 移除首尾
Map.Entry<String, Integer> removedFirst = map.pollFirstEntry(); // one=1
Map.Entry<String, Integer> removedLast = map.pollLastEntry(); // three=3
// 反转顺序
SequencedMap<String, Integer> reversedMap = map.reversed();
reversedMap.forEach((k, v) -> System.out.println(k + "=" + v)); // 如果 map 只剩 [two=2],反转不变
这些方法让处理有序集合变得非常直观,尤其是当你需要实现“最近最少使用(LRU)”缓存或“轮询”队列时。
注意:
TreeMap也是SequencedMap,但它的“第一个”和“最后一个”是根据键的自然顺序或比较器顺序,而不是插入顺序。
5. 性能考量 —— 让你的集合跑得更快
5.1 初始容量
ArrayList、HashMap、HashSet 等底层基于数组或哈希表的集合,在创建时可以指定初始容量。如果容量不够,它们会自动扩容(通常是翻倍或重新哈希),但这个操作开销较大。
如果你能提前预估元素数量,指定一个合理的初始容量,可以大幅减少扩容次数,提升性能。
// 预计存储 1000 个元素
List<String> list = new ArrayList<>(1000);
Map<String, String> map = new HashMap<>(1000);
5.2 加载因子
HashMap 和 HashSet 有一个加载因子(load factor),默认为 0.75。它表示当哈希表被填满 75% 时,就会触发扩容(rehash)。这个值是空间和时间的权衡——越小,空间利用率低但碰撞少;越大,空间利用率高但碰撞多,查找变慢。在大多数情况下,保持默认的 0.75 即可。
5.3 选择合适的数据结构
- 频繁随机访问 →
ArrayList - 频繁插入/删除(尤其头部) →
LinkedList或ArrayDeque - 需要去重 →
HashSet - 需要排序 →
TreeSet/TreeMap - 需要按插入顺序迭代 →
LinkedHashSet/LinkedHashMap
5.4 避免自动装箱开销
如果集合存储的是基本类型,使用 ArrayList<Integer> 会导致自动装箱和拆箱,产生额外的对象创建。在性能敏感的场景,考虑使用基本类型集合库(如 IntArrayList from Eclipse Collections 或 Trove)。不过对于大多数日常开发,自动装箱的开销可以忽略不计。
6. 综合示例 —— 用集合技巧实现单词统计器
让我们用今天学到的各种技巧,写一个实用的小工具:统计一段文本中每个单词出现的次数,并按出现次数从高到低排序输出。
import java.util.*;
import java.util.stream.Collectors;
public class WordCounter {
public static void main(String[] args) {
String text = "apple banana apple orange banana apple grape banana orange apple";
// 1. 使用 Map.merge() 统计词频
Map<String, Integer> wordCount = new HashMap<>();
for (String word : text.split(" ")) {
wordCount.merge(word, 1, Integer::sum);
}
// 2. 使用 entrySet() + Collections.sort() 按值排序(降序)
List<Map.Entry<String, Integer>> entries = new ArrayList<>(wordCount.entrySet());
entries.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue())); // 降序
// 3. 打印结果
System.out.println("单词统计(按出现次数降序):");
for (Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 4. 用 computeIfAbsent 实现分组(演示)
Map<Integer, List<String>> groupedByCount = new LinkedHashMap<>();
for (Map.Entry<String, Integer> entry : wordCount.entrySet()) {
groupedByCount.computeIfAbsent(entry.getValue(), k -> new ArrayList<>())
.add(entry.getKey());
}
System.out.println("\n按次数分组:");
groupedByCount.forEach((count, words) -> {
System.out.println(count + "次: " + words);
});
// 5. JDK 21 的 SequencedMap 演示
SequencedMap<String, Integer> sortedMap = new LinkedHashMap<>();
entries.forEach(e -> sortedMap.put(e.getKey(), e.getValue()));
System.out.println("\n有序 Map 的首尾:");
System.out.println("第一个单词:" + sortedMap.firstEntry());
System.out.println("最后一个单词:" + sortedMap.lastEntry());
}
}
这个例子把 merge()、排序、computeIfAbsent 和 SequencedMap 都串了起来。
7. 常见陷阱与注意事项
- 不可变集合的修改:
List.of()返回的集合是不可变的,任何修改操作都会抛出UnsupportedOperationException。如果你需要一个可变的副本,可以new ArrayList<>(List.of(...))。 removeIf配合List时注意:ArrayList的removeIf内部使用了BitSet,效率不错,但LinkedList的removeIf可能会遍历整个列表,注意性能。Map的computeIfAbsent的 value 可能为 null:如果mappingFunction返回null,键不会被插入,且返回null。这可能导致意外行为。TreeMap/TreeSet的排序:如果元素没有实现Comparable,也没有传入Comparator,会在运行时抛出ClassCastException。
8. 今天的总结
今天我们深入学习了集合框架的各种实用技巧和 JDK 21 的新特性:
Collections工具类:排序、反转、查找、不可变视图、线程安全包装。- 集合工厂方法(JDK 9+):
List.of()、Set.of()、Map.of(),快速创建不可变集合。 - 集合的高级操作(JDK 8+):
removeIf()、replaceAll()、forEach(),以及Map的computeIfAbsent、merge、getOrDefault等。 - JDK 21 的
SequencedCollection:统一了有序集合的getFirst()、getLast()、reversed()等操作。 - 性能考量:初始容量、加载因子、选择合适的数据结构。
集合框架是 Java 程序员日常工作中使用频率最高的类库之一。熟练掌握这些实用技巧,能让你的代码更简洁、更高效、更安全。
动手试试
- 创建一个不可变的
List<String>包含几个城市名,然后尝试添加一个元素,观察抛出的异常。 - 给定一个
List<Integer>,用removeIf()删除所有小于 0 的元素,并用replaceAll()将所有正数乘以 2。 - 实现一个简单的缓存:用
Map<String, String>模拟,当键不存在时,用computeIfAbsent生成一个新值(比如"value_" + key),并返回。 - 写一个方法,接收一个
List<String>,返回一个Map<String, Integer>,统计每个字符串的出现次数,并用TreeMap按键排序。 - 在你之前的学生管理系统中,把学生集合改为
Map<String, Student>(学号作为键),并利用computeIfAbsent实现“根据学号获取学生,如果不存在则创建一个默认学生”的功能。
集合框架的三篇文章到这里就结束了。下一篇文章,我们将进入 泛型(Generics) 的世界——它是 Java 类型系统的强大武器,也是集合框架背后的重要支撑。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。
:实用技巧与 JDK 21 新特性&spm=1001.2101.3001.5002&articleId=162907578&d=1&t=3&u=e2dedde27efe482abcd208e479c3b13c)
3386

被折叠的 条评论
为什么被折叠?



