31. 【Java】集合框架(下):实用技巧与 JDK 21 新特性

摘要: 本文深入讲解Java集合框架的实用技巧与最新特性,涵盖Collections工具类的排序、查找、不可变集合等核心功能,JDK 9+的集合工厂方法,JDK 8+的高级操作如removeIf()、computeIfAbsent(),以及JDK 21的Sequenced Collections统一接口。通过性能优化建议和综合示例,帮助开发者编写更简洁、高效、安全的集合代码。

关键词: Java集合, Collections工具类, JDK新特性, 不可变集合, 性能优化


经过前两篇的学习,你已经掌握了 Java 集合框架的三大核心家族:ListSetMapQueue。你知道什么时候用 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 包中的专用集合(如 ConcurrentHashMapCopyOnWriteArrayList)。

// 不推荐(老式做法)
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 开始,ListSetMap 接口都提供了静态工厂方法,让你用一行代码创建不可变的集合。

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()

对于 Mapof() 方法支持最多 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 之前,Listget(0)get(size()-1) 来获取首尾,但 Set 没有统一的方式(LinkedHashSet 虽然保持插入顺序,但没有获取首尾的方法),DequegetFirst()getLast(),但接口不一致。

JDK 21 引入了一组新接口,统一了“有序集合”的操作:

  • SequencedCollectionListDeque 以及有序的 Set(如 LinkedHashSet)都实现了它。
  • SequencedSetSequencedCollection 的子接口,专门给有序 Set 用的。
  • SequencedMap:有序 Map(如 LinkedHashMapTreeMap)实现的接口。
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]
}

对于 ListgetFirst() 等价于 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 初始容量

ArrayListHashMapHashSet 等底层基于数组或哈希表的集合,在创建时可以指定初始容量。如果容量不够,它们会自动扩容(通常是翻倍或重新哈希),但这个操作开销较大

如果你能提前预估元素数量,指定一个合理的初始容量,可以大幅减少扩容次数,提升性能。

// 预计存储 1000 个元素
List<String> list = new ArrayList<>(1000);
Map<String, String> map = new HashMap<>(1000);
5.2 加载因子

HashMapHashSet 有一个加载因子(load factor),默认为 0.75。它表示当哈希表被填满 75% 时,就会触发扩容(rehash)。这个值是空间和时间的权衡——越小,空间利用率低但碰撞少;越大,空间利用率高但碰撞多,查找变慢。在大多数情况下,保持默认的 0.75 即可。

5.3 选择合适的数据结构
  • 频繁随机访问 → ArrayList
  • 频繁插入/删除(尤其头部) → LinkedListArrayDeque
  • 需要去重 → 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()排序computeIfAbsentSequencedMap 都串了起来。


7. 常见陷阱与注意事项

  • 不可变集合的修改List.of() 返回的集合是不可变的,任何修改操作都会抛出 UnsupportedOperationException。如果你需要一个可变的副本,可以 new ArrayList<>(List.of(...))
  • removeIf 配合 List 时注意ArrayListremoveIf 内部使用了 BitSet,效率不错,但 LinkedListremoveIf 可能会遍历整个列表,注意性能。
  • MapcomputeIfAbsent 的 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(),以及 MapcomputeIfAbsentmergegetOrDefault 等。
  • JDK 21 的 SequencedCollection:统一了有序集合的 getFirst()getLast()reversed() 等操作。
  • 性能考量:初始容量、加载因子、选择合适的数据结构。

集合框架是 Java 程序员日常工作中使用频率最高的类库之一。熟练掌握这些实用技巧,能让你的代码更简洁、更高效、更安全。


动手试试

  1. 创建一个不可变的 List<String> 包含几个城市名,然后尝试添加一个元素,观察抛出的异常。
  2. 给定一个 List<Integer>,用 removeIf() 删除所有小于 0 的元素,并用 replaceAll() 将所有正数乘以 2。
  3. 实现一个简单的缓存:用 Map<String, String> 模拟,当键不存在时,用 computeIfAbsent 生成一个新值(比如 "value_" + key),并返回。
  4. 写一个方法,接收一个 List<String>,返回一个 Map<String, Integer>,统计每个字符串的出现次数,并用 TreeMap 按键排序。
  5. 在你之前的学生管理系统中,把学生集合改为 Map<String, Student>(学号作为键),并利用 computeIfAbsent 实现“根据学号获取学生,如果不存在则创建一个默认学生”的功能。

集合框架的三篇文章到这里就结束了。下一篇文章,我们将进入 泛型(Generics) 的世界——它是 Java 类型系统的强大武器,也是集合框架背后的重要支撑。

我们下一篇见。😃

📌 获取本系列示例代码请访问 GitCode

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值