Skip to content

Java 集合框架 ​

说明:本文代码均为示例片段(Illustrative fragment,演示语法与用法,未逐一编译运行);需要让读者知道结果的地方,以代码注释标注可观测结果。版本基线为 Java 17。

目录 ​

如何选择集合 ​

先按"需求 → 推荐实现"定位,再到对应小节看 API 与陷阱:

需求推荐实现
有序可重复列表、随机访问ArrayList
频繁头尾插入删除 / 双端队列LinkedList
读多写少的线程安全列表CopyOnWriteArrayList
去重、不关心顺序HashSet
去重且保持插入顺序LinkedHashSet
去重且按大小排序、需要范围查询TreeSet
线程安全去重集合ConcurrentHashMap.newKeySet()
线程安全且排序的集合ConcurrentSkipListSet
键值映射、快速查找HashMap
键值映射且保持插入顺序 / LRULinkedHashMap
键值映射且按 key 排序、需要范围查询TreeMap
线程安全键值映射ConcurrentHashMap
线程安全且按 key 排序ConcurrentSkipListMap
枚举作键 / 元素EnumMap / EnumSet
不可变常量集合List.of / Set.of / Map.of
栈 / 队列(单线程)ArrayDeque
生产者-消费者阻塞队列LinkedBlockingQueue

List ​

特点:有序、可重复

ArrayList ​

有了数组,为什么还要有集合?

  • 长度可变:数组长度创建后即固定,集合可随增删动态伸缩;
  • 统一而丰富的 API:数组的插入、删除、查找、排序都要自己写,集合提供了 add/remove/contains/sort/subList 等现成方法;
  • 多样的数据结构:数组只有"连续存储 + 下标访问"一种形态,集合族提供 List/Set/Map/Queue/Deque 等多种结构,匹配不同访问模式(去重、映射、排队、排序等)。

关于"类型":数组和泛型集合通常都只存一种元素类型(List<String> 仍是单一类型)。集合的价值不在"能装多种类型",而在可变长度、统一 API 与丰富结构;泛型则把类型检查提前到编译期。

因此,在实际编码中,除非有明确的性能或内存顾虑,否则优先使用集合。

常用 API ​

方法说明
add(E e)在 List 集合尾部添加元素
add(int index, E element)在指定位置添加元素
addAll(Collection c)将另一个集合的所有元素追加到尾部
remove(int index)根据索引位置移除元素
remove(Object o)根据元素内容移除元素
removeIf(Predicate filter)移除所有满足条件的元素
get(int index)根据索引位置获取元素
set(int index, E element)根据索引位置修改元素
replaceAll(UnaryOperator op)对每个元素执行操作并原地替换
indexOf(Object o)返回元素索引位置
contains(Object o)是否包含某元素
subList(int from, int to)返回 [from, to) 范围的视图,修改视图影响原 List
sort(Comparator c)按比较器原地排序
forEach(Consumer action)遍历每个元素
size()返回 List 集合大小
clear()清空 List 集合元素
isEmpty()判断 List 集合是否为空
toArray()将 List 集合转换成数组
toArray(IntFunction generator)转换成指定类型的数组,如 toArray(String[]::new)
java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ArrayListDemo {
    public static void main(String[] args) {
        // 基本操作:添加、删除、判断
        List<String> students = new ArrayList<>();
        students.add("张三");
        students.add("李四");
        students.add("王五");
        students.remove("李四");

        boolean hasZhang = students.contains("张三");
        int size = students.size();
        System.out.println(hasZhang); // true
        System.out.println(size);     // 2

        // 条件删除:移除所有偶数
        List<Integer> list = new ArrayList<>(List.of(3, 1, 4, 1, 5));
        list.removeIf(n -> n % 2 == 0);

        // 原地排序
        list.sort(Comparator.naturalOrder());

        // 每个元素翻倍
        list.replaceAll(n -> n * 2);

        // 视图操作:只操作前 2 个元素
        List<Integer> head = list.subList(0, 2);
        head.clear(); // 原 list 的前 2 个元素也被删除

        // 转成指定类型数组(List.of 详见下文「不可变 List」)
        String[] arr = List.of("A", "B").toArray(String[]::new);
    }
}

subList 视图陷阱 ​

subList(from, to) 返回的是父列表的视图(backed by 原列表),不是独立副本——读写视图都会作用到父列表。三条必须记住的规则:

  • 父列表结构性修改后,视图可能失效:对父列表 add/remove(改变 size)后再操作子列表,可能抛 ConcurrentModificationException;
  • 需要长期保存 / 异步传递 / 分批处理时,做防御性拷贝:用 new ArrayList<>(source.subList(from, to)) 脱钩;
  • 校验下标与分页参数:保证 0 <= from <= to <= size(),对空列表、页码越界、页大小非正数先兜底。
java
List<Integer> source = new ArrayList<>(List.of(0, 1, 2, 3, 4, 5));

// 分页取第 2 页(每页 2 条):下标 [from, to)
int page = 2, size = 2;
int from = Math.max(0, (page - 1) * size);
int to = Math.min(source.size(), from + size);      // 夹逼,防止 to 越界
List<Integer> pageView = source.subList(from, to);  // 视图:[2, 3]

// 父列表结构性修改后,继续用旧视图有风险
source.add(6);
// pageView.add(99); // 可能抛 ConcurrentModificationException

// 要独立副本(长期保存 / 传给异步任务 / 分批处理):
List<Integer> snapshot = new ArrayList<>(source.subList(from, to)); // 与父列表脱钩

遍历中安全删除(fail-fast) ​

fail-fast(快速失败) = 一旦发现集合被"绕过迭代器"偷偷修改了,迭代器立刻崩溃报错,不带着脏数据继续跑,避免后续出现更难排查的问题。

因此 for 循环里直接 list.remove(...) 是错的,应改用 removeIf。

java
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4));

// ❌ 错误:for-each 中直接删除,下一步迭代触发 fail-fast
// for (Integer n : list) {
//     if (n % 2 == 0) list.remove(n); // 抛 ConcurrentModificationException
// }

// ✅ 推荐:removeIf(Java 8+,最简洁)
list.removeIf(n -> n % 2 == 0); // 移除偶数,剩下 [1, 3]

CopyOnWriteArrayList 是例外:它的迭代器是创建时的快照,遍历期间其他线程的修改既不可见、也不会抛 CME(详见上文 CopyOnWriteArrayList 小节)。

装箱陷阱:remove(int) 与 remove(Object) ​

对 List<Integer> 调用 remove(1) 时,编译器优先匹配 remove(int index)(按下标删除),而不是 remove(Object)(按值删除)。要按值删除必须显式装箱。

java
List<Integer> nums = new ArrayList<>(List.of(10, 20, 30));
nums.remove(1);                    // 命中 remove(int index):删除下标 1 的元素 20 → [10, 30]

List<Integer> vals = new ArrayList<>(List.of(10, 20, 30));
vals.remove(Integer.valueOf(10));  // 命中 remove(Object):删除"值 10" → [20, 30]

LinkedList ​

LinkedList 基于双向链表实现,同时实现了 List 和 Deque 接口,因此既可以作为列表使用,也可以作为栈/队列使用。

特性说明
底层结构双向链表(每个节点存储 prev、next 指针和元素值)
随机访问 get(i)O(n),需从头/尾遍历到指定位置
头部/尾部操作O(1),直接操作头/尾节点指针
中间插入/删除O(n)(定位)+ O(1)(修改指针),但定位后修改本身很快
内存开销每个元素额外存储两个指针(prev/next),比 ArrayList 的紧凑数组更占内存
缓存友好性差(节点在堆上分散分配,CPU 缓存命中率低)
线程安全否,需要线程安全可用 Collections.synchronizedList(new LinkedList<>()) 或 CopyOnWriteArrayList
null 元素允许

完整 API ​

List 接口方法(与 ArrayList 相同)

方法说明
add(E e) / add(int i, E e)尾部/指定位置插入
get(int i)按下标获取(O(n))
set(int i, E e)修改指定位置元素
remove(int i) / remove(Object o)按下标/按值删除
size() / isEmpty() / contains()基本操作
indexOf(Object o) / lastIndexOf()查找元素位置(需遍历)

Deque 接口方法(LinkedList 特有优势)

方法等价队列方法说明
addFirst(e)offerFirst(e)头部插入(栈 push / 队首)
addLast(e)offerLast(e)尾部插入(队尾)
removeFirst()pollFirst()头部删除(栈 pop / 出队)
removeLast()pollLast()尾部删除
getFirst()peekFirst()获取头部(不删除)
getLast()peekLast()获取尾部(不删除)
push(e) / pop()同 addFirst/removeFirst栈操作

企业开发建议:需要栈/队列行为时优先用 ArrayDeque(数组实现,缓存友好,性能优于 LinkedList)。LinkedList 的实际使用场景很窄,仅在需要频繁在两端操作且元素量大、或明确需要链表结构时才考虑。

java
import java.util.LinkedList;

// 作为双端队列使用
LinkedList<String> deque = new LinkedList<>();
deque.addFirst("A");  // 头部插入
deque.addLast("B");   // 尾部插入
deque.addFirst("C");  // ["C", "A", "B"]

String first = deque.removeFirst(); // "C"
String last = deque.removeLast();   // "B"

// 作为栈使用
LinkedList<String> stack = new LinkedList<>();
stack.push("top");    // 入栈
String top = stack.pop(); // 出栈

CopyOnWriteArrayList ​

CopyOnWriteArrayList 是线程安全的 List,采用写时复制策略:每次写操作(add/set/remove)都会复制一份底层数组,读操作则完全不加锁。

  • 适用场景:读多写少,如事件监听器列表、配置缓存、观察者模式;
  • 不适用:元素量大或写操作频繁(复制数组开销高),此时考虑 Collections.synchronizedList 或加锁;
  • 注意:迭代器是创建时的快照,遍历期间其他线程的修改不可见,也不会抛 ConcurrentModificationException。

常用 API 与 ArrayList 基本一致,这里只列出差异和特有方法:

方法说明
add(E e) / add(int index, E e)线程安全地添加元素
addIfAbsent(E e)元素不存在时才添加(特有)
set(int index, E element)线程安全地修改元素
remove(int index) / remove(Object o)线程安全地移除元素
get(int index) / size() / isEmpty()读操作不加锁
java
import java.util.concurrent.CopyOnWriteArrayList;

// 典型场景:监听器列表,注册少、通知多
CopyOnWriteArrayList<EventListener> listeners = new CopyOnWriteArrayList<>();

// 多线程并发注册,无需额外加锁
listeners.add(listener);
listeners.addIfAbsent(listener); // 避免重复注册

// 遍历时不会被其他线程的修改干扰
for (EventListener l : listeners) {
    l.onEvent(event);
}

List 创建方式对比 ​

创建 List 时,可变性与"是否回写源"差别很大。不可变 List(List.of / List.copyOf / stream().toList())适合用作常量配置、方法返回值(防止调用方篡改);可变 List(new ArrayList<>(...) / Arrays.asList(...))适合需要后续增删改的场景。

创建方式底层能否增删set 改允许 null与源的关系
new ArrayList<>(coll)独立数组✅✅✅完全独立(快照)
Arrays.asList(arr)包装原数组❌ 抛 UOE✅ 会写回 arr✅双向视图
List.of(...)不可变❌❌❌ 抛 NPE独立快照
List.copyOf(coll)不可变❌❌❌独立快照
stream().toList()(16+)不可变❌❌✅ 允许 null独立

UOE = UnsupportedOperationException,NPE = NullPointerException。

不可变 List 用法示例:

java
// 场景 1:定义常量配置(防止被意外修改)
public class Config {
    public static final List<String> VALID_LEVELS = List.of("INFO", "WARN", "ERROR");
    public static final List<String> SUPPORTED_OS = List.of("Windows", "Linux", "macOS");
}

// 场景 2:方法返回不可变集合(防止调用方篡改内部数据)
public List<String> getStatusCodes() {
    List<String> internal = new ArrayList<>(List.of("200", "404", "500"));
    return List.copyOf(internal); // 返回副本,调用方无法修改内部状态
}

// 场景 3:试图修改会抛异常
List<String> immutable = List.of("A", "B", "C");
// immutable.add("D");      // UnsupportedOperationException
// immutable.remove("A");   // UnsupportedOperationException
// immutable.set(0, "X");   // UnsupportedOperationException

三个高频坑:

java
// 坑 1:Arrays.asList 是"定长视图",增删抛异常,但 set 会写回原数组
Integer[] arr = {1, 2, 3};
List<Integer> fixed = Arrays.asList(arr);
// fixed.add(4);   // UnsupportedOperationException
fixed.set(0, 9);   // ✅ 允许改;此时 arr[0] 也变成了 9

// 坑 2:基本类型数组装箱——得到 size()==1 的 List<int[]>,而非 List<Integer>
int[] raw = {1, 2, 3};
List<int[]> wrong = Arrays.asList(raw);                  // wrong.size() == 1(整个数组当成 1 个元素)
List<Integer> right = Arrays.stream(raw).boxed().toList(); // [1, 2, 3]

// 坑 3:List.of / List.copyOf 不允许 null
// List.of("a", null); // NullPointerException

// 需要"基于现有数据但可增删"时,复制成 ArrayList:
List<Integer> mutable = new ArrayList<>(Arrays.asList(1, 2, 3));
mutable.add(4); // ✅ [1, 2, 3, 4]

Set ​

特点:元素不重复。按遍历顺序分三类:

  • 无序:HashSet —— 不保证任何顺序,性能最好(O(1));
  • 有序(insertion-ordered):LinkedHashSet —— 按插入先后遍历;
  • 排序(sorted):TreeSet / ConcurrentSkipListSet —— 按元素大小(Comparable/Comparator)遍历,支持范围查询。

关键区分:"有序"指保持放入的先后,"排序"指按元素大小排列,二者不是一回事。

HashSet ​

HashSet 用于存储不重复的元素,且不保证顺序,常用于需要快速去重、快速包含性检查的场景。

常用 API ​

方法说明
add(E e)添加元素,若已存在则返回 false
remove(Object o)移除元素,存在则返回 true
removeAll(Collection c)移除当前集合中那些也在 c 中存在的元素(差集)
retainAll(Collection c)只保留同时存在于当前集合和 c 中的元素(交集)
removeIf(Predicate filter)移除所有满足条件的元素
contains(Object o)判断是否包含该元素
addAll(Collection c)批量添加元素(并集)
forEach(Consumer action)遍历每个元素
size()返回元素个数
isEmpty()判断是否为空
clear()清空所有元素
java
// 基本操作
Set<Long> ids = new HashSet<>();
ids.add(1L);
ids.add(2L);
ids.add(1L); // 重复,不会添加
System.out.println(ids.size()); // 2
System.out.println(ids.contains(1L)); // true

// 1. 去重
List<Long> list = List.of(1L, 2L, 2L, 3L);
Set<Long> uniqueIds = new HashSet<>(list); // [1, 2, 3]

// 2. 判断是否存在(快速)
Set<String> bannedWords = new HashSet<>(List.of("spam", "ad"));
if (bannedWords.contains(word)) {
    // 过滤
}

// 3. 集合运算
Set<String> set1 = new HashSet<>(List.of("A", "B", "C"));
Set<String> set2 = new HashSet<>(List.of("B", "C", "D"));

// 交集
set1.retainAll(set2); // [B, C]

// 并集
set1.addAll(set2); // [A, B, C, D]

// 差集
set1.removeAll(set2); // [A]

LinkedHashSet ​

LinkedHashSet 继承自 HashSet,内部额外维护了一个双向链表记录插入顺序,因此遍历顺序与插入顺序一致。常用 API 与 HashSet 完全相同。

适用场景:需要去重且必须保持插入顺序,例如 JSON 字段去重、按提交顺序展示的唯一记录。

java
Set<String> visited = new LinkedHashSet<>();
visited.add("/home");
visited.add("/list");
visited.add("/home"); // 重复,不会添加,也不改变原有顺序

System.out.println(visited); // [/home, /list]

TreeSet ​

TreeSet 是排序 Set:基于红黑树,元素按 Comparable 自然顺序或构造时传入的 Comparator 排列,增删查为 O(log n)。它实现 NavigableSet,提供范围查询能力——这是它区别于 HashSet/LinkedHashSet 的核心价值。不允许 null(无法比较)。

API 与 HashSet 的关系:TreeSet 包含 HashSet 的所有 API,但额外实现了 NavigableSet 接口,提供了范围查询方法:

方法说明
first() / last()最小 / 最大元素
floor(e) / ceiling(e)<= e 的最大值 / >= e 的最小值
higher(e) / lower(e)严格 > e / 严格 < e
subSet(from, to)范围视图 [from, to)
headSet(to) / tailSet(from)< to / >= from
pollFirst() / pollLast()获取并移除最小 / 最大元素
java
// 自然顺序(元素实现 Comparable)
TreeSet<Integer> nums = new TreeSet<>(List.of(5, 1, 3, 1));
// nums = [1, 3, 5](自动排序 + 去重)

// 自定义排序:按字符串长度(Comparator.comparingInt 详见下文"排序"节)
TreeSet<String> byLength = new TreeSet<>(Comparator.comparingInt(String::length));
byLength.addAll(List.of("a", "bbb", "cc"));
// byLength = ["a", "cc", "bbb"](按长度 1, 2, 3 排序)

// NavigableSet 范围查询
nums.first();      // 1(最小)
nums.last();       // 5(最大)
nums.floor(4);     // 3(<= 4 的最大值)
nums.ceiling(4);   // 5(>= 4 的最小值)
nums.higher(3);    // 5(严格 > 3)
nums.lower(3);     // 1(严格 < 3)
nums.subSet(2, 5); // [3](范围视图 [2, 5))
nums.headSet(3);   // [1](< 3)
nums.tailSet(3);   // [3, 5](>= 3)

适用:需要去重且始终有序、或频繁做"最接近值 / 范围"查询(如排行榜名次、时间窗口)。仅需去重、不关心顺序时用 HashSet(O(1) 更快)。

ConcurrentHashMap.newKeySet() ​

用来快速创建一个线程安全的 Set。它的底层直接由 ConcurrentHashMap 支持,因此具备高并发性能。

原理:Set 本质是"只有 key、没有业务 value 的 Map"。newKeySet() 内部创建了一个 ConcurrentHashMap,并为每个加入 Set 的元素放入同一个固定的占位 value(可以把它理解为“存在”标记)。因此,元素只要是 Map 的 key,就表示它在 Set 中:

Set 操作内部实现
add(e)map.put(e, PRESENT)(PRESENT 是一个固定的占位对象)
remove(e)map.remove(e)
contains(e)map.containsKey(e)
size()map.size()

所以它不是给普通 HashSet 外面简单套一把锁;多个线程可同时对不同 key 进行操作,底层通过 CAS、细粒度同步等机制保证单个操作的线程安全。遍历时使用的是弱一致性迭代器:不会抛 ConcurrentModificationException,但迭代过程中不保证一定看见其他线程刚加入或删除的每一个元素。

null 处理:与 ConcurrentHashMap 一致,不允许 null 元素(add(null) 抛 NPE)。

java
import java.util.*;
import java.util.concurrent.*;

// 8 个线程同时写入:每个线程都写入 0 ~ 49_999。
// 总共发起 400_000 次 add,但 Set 最终只能保留 50_000 个不同元素。
// 以下代码放在声明了 throws Exception 的 main 方法(或自行处理异常)中运行。

// 创建由 ConcurrentHashMap 支撑的线程安全 Set;元素本身就是 Map 的 key。
Set<Integer> processedIds = ConcurrentHashMap.newKeySet();
int threadCount = 8;                  // 并发工作线程数
int idsPerThread = 50_000;            // 每个线程尝试加入的 id 数量

// 固定大小线程池:最多同时运行 8 个提交的任务。
ExecutorService pool = Executors.newFixedThreadPool(threadCount);

// ready 初值为 8:每个任务就绪后减 1;主线程等待它变为 0。
CountDownLatch ready = new CountDownLatch(threadCount);
// start 初值为 1:所有任务先在此等待;主线程 countDown 后统一放行。
CountDownLatch start = new CountDownLatch(1);
// 保存 Future,后续可等待每个任务真正结束,并接收任务中的异常。
List<Future<?>> futures = new ArrayList<>();

for (int t = 0; t < threadCount; t++) {
    futures.add(pool.submit(() -> {
        ready.countDown();             // 报告“我已创建并准备完毕”
        start.await();                 // 如果计数还是 1,就在这里等待
        for (int id = 0; id < idsPerThread; id++) {
            // add 是原子操作:首次加入返回 true;重复加入返回 false,不会产生重复元素。
            processedIds.add(id);
        }
        return null;                   // Callable<Void> 的正常结束标记
    }));
}

ready.await();                         // 主线程确认 8 个任务都到达“起跑线”
start.countDown();                     // 把计数从 1 减到 0,让所有正在 start.await() 等待的工作线程都会被唤醒并继续执行
for (Future<?> future : futures) {
    future.get();                      // 等待该任务结束;任务抛异常时会在这里暴露
}
pool.shutdown();                       // 不再接受新任务;已提交的任务已全部结束

// 400,000 次并发 add 完成后,每个 id 只保留一份,而不是 400,000 份。
System.out.println(processedIds.size());        // 50000
System.out.println(processedIds.contains(123)); // true:任一线程都已加入过 123

上例中,8 个线程对相同的 50,000 个 key 并发调用 add。尽管调用次数是 400,000,完成后集合大小稳定为 50,000,且每个 id 只存在一份;这就是它作为并发去重 Set 的直接表现。若改用 HashSet 并让多个线程这样写,结果不可靠,甚至可能出现结构损坏或异常。

注意:单次 add、remove、contains 都是线程安全的,但多个调用拼成的业务步骤不是一个整体原子操作。对于“仅当第一次加入成功时才执行”的需求,不要先 contains 再 add,直接检查 add 的返回值:if (processedIds.add(id)) { /* 第一次处理该 id */ }。更复杂的“检查 + 修改”逻辑可改用 ConcurrentHashMap.compute / merge,或按业务需要加锁。

常用 API 与 HashSet 相同(add / remove / contains / size),常用于并发去重、在线用户集合、已处理任务记录等场景。

ConcurrentSkipListSet ​

需要线程安全 + 排序的 Set 时用 ConcurrentSkipListSet:基于跳表实现,相当于 TreeSet 的并发版。元素按 Comparable 自然顺序或构造时传入的 Comparator 排列,增删查为 O(log n);同样实现 NavigableSet,支持 first/last/floor/ceiling/higher/lower/subSet 等范围查询。

java
import java.util.NavigableSet;
import java.util.concurrent.ConcurrentSkipListSet;

// 并发 + 排序:如实时排行榜、按时间戳排序的事件集合
NavigableSet<Long> timestamps = new ConcurrentSkipListSet<>();
timestamps.add(ts);            // 线程安全,且按 ts 排序
timestamps.first();            // 最小(最早)
timestamps.last();             // 最大(最晚)
timestamps.tailSet(t0);        // >= t0 的子集(范围查询)

选型提示:只需"并发去重"、不关心顺序时用 ConcurrentHashMap.newKeySet()(更快);既要并发又要始终有序 / 范围查询时才用 ConcurrentSkipListSet(跳表有多层指针,内存开销更大)。

不可变 Set ​

创建方式说明
Set.of(e1, e2, ...)直接创建不可变集合,元素不允许 null、不允许重复
Set.copyOf(collection)从已有集合复制出不可变副本
java
Set<String> roles = Set.of("admin", "user");
// roles.add("guest"); // UnsupportedOperationException

Set<String> copy = Set.copyOf(existingSet);

Map ​

HashMap ​

哈希表(Hash Table)是一种数据结构,它利用哈希函数将键映射到特定的值,使用键快速检索数据。存储的数据结构是无序的。

常用 API ​

方法说明
put(K key, V value)存入键值对,相同 key 会覆盖旧值
putIfAbsent(K key, V value)key 不存在时才放入,已存在则不覆盖
get(Object key)根据 key 取值,不存在返回 null
getOrDefault(Object key, V default)取不到时返回默认值,避免 NPE
remove(Object key)根据 key 移除键值对
containsKey(Object key)是否包含指定 key
containsValue(Object value)是否包含指定 value
entrySet()返回所有键值对的 Set 视图
forEach(BiConsumer action)遍历所有键值对
keySet()返回所有 key 的 Set 视图
values()返回所有 value 的 Collection 视图
putAll(Map m)批量放入另一个 Map 的所有键值对
replace(K key, V value)替换已存在 key 的值
replaceAll(BiFunction function)对所有值批量转换
merge(K key, V value, BiFunction remappingFunction)key 不存在直接放入,已存在则合并新旧值
computeIfAbsent(K key, Function mappingFunction)key 不存在时计算 value 并存入
size()返回键值对数量
isEmpty()判断是否为空
clear()清空所有键值对
基本操作 ​
java
Map<String, String> map = new HashMap<>();
map.put("1", "One");
map.put("2", "Two");
map.put("3", "Three");

String value = map.get("1");        // "One"
map.remove("2");                     // 移除 key="2" 的键值对
boolean has = map.containsKey("3"); // true
安全取值 ​
java
// 取不到时返回默认值,避免 NPE
String name = userMap.getOrDefault("name", "未知用户");

// key 不存在时才执行 lambda 计算并存入;已存在则直接返回旧值、lambda 不执行(典型场景:缓存)
// 未命中:cacheMap 无 "user:1001" → 调用 findFromDB 查库 → 存入并返回结果
// 命中:  cacheMap 有 "user:1001" → 直接返回缓存值,findFromDB 不被调用
User user = cacheMap.computeIfAbsent("user:1001", key -> userService.findFromDB(key));
遍历 ​
java
// 方式一:for-each entrySet
for (Map.Entry<String, String> entry : userMap.entrySet()) {
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}

// 方式二:forEach(更简洁)
userMap.forEach((key, value) -> System.out.println(key + " -> " + value));
合并与更新 ​
java
// putAll:批量合并(相同 key 会被覆盖)
Map<String, String> target = new HashMap<>(Map.of("name", "张三"));
Map<String, String> source = Map.of("name", "李四", "age", "25");
target.putAll(source); // {name=李四, age=25}

// replace:替换已存在 key 的值
map.replace("苹果", 10);

// replaceAll:批量转换所有值
prices.replaceAll((name, price) -> price * 0.8); // 所有商品打 8 折

// merge:key 不存在直接放入,已存在则合并(典型场景:计数、分组累加)
Map<String, Integer> wordCount = new HashMap<>();
for (String word : words) {
    wordCount.merge(word, 1, Integer::sum); // 不存在则置 1,已存在则 +1
}
computeIfAbsent:分组与建树 ​

computeIfAbsent 最经典的用法是"按 key 初始化容器":key 不存在时先放一个空容器(如 ArrayList)再往里塞元素,省去"先判空 → new → put"的样板。

分组(一对多):

java
// 把订单按用户分组
Map<Long, List<Order>> ordersByUser = new HashMap<>();
for (Order o : orders) {
    ordersByUser.computeIfAbsent(o.userId(), k -> new ArrayList<>()).add(o);
}
// 等价流式写法:orders.stream().collect(Collectors.groupingBy(Order::userId))

平铺记录 → 父子树(两阶段构建):

java
record Node(long id, long parentId, String name) {}
// TreeNode:树节点,持有原记录与子节点列表
class TreeNode {
    final Node node;
    final List<TreeNode> children = new ArrayList<>();
    TreeNode(Node node) { this.node = node; }
}

List<Node> rows = List.of(
    new Node(1, 0, "根"),
    new Node(2, 1, "子A"),
    new Node(3, 1, "子B"),
    new Node(4, 2, "孙A1"));

// 阶段一:按 parentId 分组——computeIfAbsent 负责"没有列表就 new 一个"
Map<Long, List<Node>> childrenByParent = new HashMap<>();
for (Node n : rows) {
    childrenByParent.computeIfAbsent(n.parentId(), k -> new ArrayList<>()).add(n);
}
// childrenByParent = {0=[根], 1=[子A, 子B], 2=[孙A1]}

// 阶段二:借助 id 索引,把分组结果装配成父子树
Map<Long, TreeNode> index = new HashMap<>();
for (Node n : rows) index.put(n.id(), new TreeNode(n)); // 先全部建节点
TreeNode root = null;
for (Node n : rows) {
    TreeNode cur = index.get(n.id());
    if (n.parentId() == 0) root = cur;                 // parentId=0 视为根
    else index.get(n.parentId()).children.add(cur);    // 挂到父节点
}
// 装配后的树形:
// 根(1)
// ├── 子A(2)
// │   └── 孙A1(4)
// └── 子B(3)

computeIfAbsent 两个坑:① mapping 函数内不要再修改同一个 map(递归更新可能死循环或抛 IllegalStateException);② mapping 函数返回 null 时不会建立映射(下次仍会重算)。

作为 key 的合约:equals / hashCode ​

当对象被用作 HashMap 的 key、HashSet 的元素,或 Stream.distinct() 的输入时,集合需要回答一个问题:两个对象是否代表同一个业务对象? equals / hashCode 就是这个判断的规则。

两者分工不同,HashMap 会先用快的 hashCode 缩小查找范围,再用准确的 equals 做最终确认:

方法用来做什么在 HashMap / HashSet 中的作用
hashCode()为对象计算一个整数哈希值;它不是唯一身份证,不同对象也可能得到相同值根据哈希值快速定位到可能存放该 key 的桶,减少需要逐个比较的对象数量
equals(Object obj)按业务规则判断两个对象是否相等在同一桶中确认候选对象是否真的是同一个 key;相等则覆盖 value(Map)或拒绝重复加入(Set)

例如用订单号作为 key 时,hashCode() 像先按订单号把记录送到某个抽屉,equals() 像在抽屉里核对订单号是否真的相同。哈希值相同不代表对象一定相等,所以不能只依赖 hashCode()。

若不重写这两个方法,继承自 Object 的默认语义是“是否为内存中的同一个实例”(近似 ==)。字段看起来相同、但分别 new 出来的对象仍是两个不同的 key:

java
// 没有重写 equals/hashCode 的普通类:仍使用 Object 的“同一实例”语义。
class User {
    private final Long id;
    private final String name;

    User(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

User a = new User(1L, "张三");
User b = new User(1L, "张三");

// 未重写 equals/hashCode 时:a.equals(b) 为 false
// 因而把 a、b 放进 HashSet,集合大小会是 2。

只有业务上要求“指定字段相同就算同一个 key”时,才需要定义自己的规则。例如订单号相同就应代表同一订单:

java
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

// 示例片段:OrderKey 是不可变的,且只按 orderId 判断是否为同一订单。
final class OrderKey {
    private final Long orderId;

    OrderKey(Long orderId) {
        this.orderId = orderId;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;                 // 同一实例必然相等
        if (!(obj instanceof OrderKey other)) return false;
        return Objects.equals(this.orderId, other.orderId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(orderId);                 // 与 equals 使用同一字段
    }
}

Map<OrderKey, String> orders = new HashMap<>();
orders.put(new OrderKey(1001L), "已支付");
orders.put(new OrderKey(1001L), "已发货"); // 覆盖同一个业务 key 的 value

System.out.println(orders.size());                    // 1
System.out.println(orders.get(new OrderKey(1001L)));  // 已发货

通常不必手写:String、Integer、LocalDate 等 JDK 值对象已经实现了规则;record 会基于全部组件自动生成一致的 equals / hashCode,很适合作为 key。普通类也可让 IDE 生成,或使用 Lombok 的 @EqualsAndHashCode。只有需要只按部分字段(如只按 orderId,不按状态、金额)判断相等时,才应明确选择这些字段并自行配置生成规则。

必须遵守两条约束:

  • a.equals(b) 为 true 时,a.hashCode() == b.hashCode() 必须成立;反过来不要求。否则相等对象会落入不同哈希桶,导致查找和去重失效。
  • key 放入 HashMap / HashSet 后,不要修改参与 equals / hashCode 的字段;否则它可能仍留在旧桶,却按新哈希值查找,形成“存得进、取不出”。因此长期 key 优先使用 record、String、包装类型或专门的不可变 key 类。

同理,HashSet 去重与 list.stream().distinct() 也依赖 equals / hashCode:不重写就按对象引用判重,"看起来一样"的元素不会被去掉。

LinkedHashMap ​

LinkedHashMap 继承自 HashMap,常用 API 与 HashMap 完全相同,在 HashMap 的基础上内部额外维护了一个双向链表,用于记录元素的插入顺序。因此遍历 LinkedHashMap 时,顺序与插入顺序一致。

适用场景:缓存实现(LRU 算法)

java
// 1. 保持配置项顺序
Map<String, String> config = new LinkedHashMap<>();
config.put("host", "localhost");
config.put("port", "8080");
config.put("timeout", "30");
// 遍历时顺序与插入时一致

// 2. JSON 字段顺序保持(配合 Jackson 等库)
Map<String, Object> json = new LinkedHashMap<>();
json.put("id", 1);
json.put("name", "张三");
json.put("age", 25);
// 序列化后字段顺序与 put 顺序一致

// 3. LRU 缓存(访问顺序模式)
// 构造器参数:initialCapacity, loadFactor, accessOrder(true=按访问顺序)
Map<Integer, String> lruCache = new LinkedHashMap<>(16, 0.75f, true);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.get(1); // 访问 key=1,它会被移到链表末尾
// 此时遍历顺序:{2=B, 1=A}(1 被访问后移到最后)

TreeMap ​

TreeMap 是排序 Map:按 key 的自然顺序(Comparable)或构造时传入的 Comparator 排列,增删查为 O(log n),实现 NavigableMap 提供范围查询。key 不允许 null(无法比较),value 可以为 null。

java
// 按 key 自然顺序排列
TreeMap<String, Integer> scores = new TreeMap<>();
scores.put("bob", 85);
scores.put("alice", 92);
scores.put("cathy", 78);
// 遍历按 key 排序:alice=92, bob=85, cathy=78

// NavigableMap 范围查询
scores.firstKey();               // "alice"(最小 key)
scores.lastKey();                // "cathy"(最大 key)
scores.floorKey("b");            // "alice"(<= "b" 的最大 key)
scores.ceilingKey("b");          // "bob"(>= "b" 的最小 key)
scores.subMap("alice", "cathy"); // {alice=92, bob=85}(范围视图 [from, to))
scores.headMap("bob");           // {alice=92}(key < "bob")
scores.tailMap("bob");           // {bob=85, cathy=78}(key >= "bob")

适用:需要按 key 有序遍历、或做范围/邻近查询(如按时间戳索引、分数段统计、字典序前缀)。仅做键值查找、不关心顺序时用 HashMap(O(1) 更快)。

ConcurrentHashMap ​

ConcurrentHashMap 常用 API 与 HashMap 完全相同,区别在于:

  • 线程安全:并发读写可安全执行;针对同一 key 的写操作(putIfAbsent、compute*、merge 等)具备原子性。但跨 key 操作、containsKey 后再 put 等多步复合逻辑不具备整体原子性(见下文"原子性与复合操作陷阱")
  • 不允许 null:键和值都不允许为 null
  • 迭代器 fail-safe:不会抛出 ConcurrentModificationException(弱一致性,见下文"迭代器弱一致")

原子性与复合操作陷阱 ​

什么是原子操作:原子 = 不可分割——一个操作要么整个完成、要么整个没做,中间状态对其他线程不可见,也不会"做到一半被别的线程插队"。

ConcurrentHashMap 对单个 key 的写操作(put / putIfAbsent / replace / compute* / merge)都是原子的:它在锁住该 key 所在的桶(bucket)的范围内完成"读—改—写",因此同一 key 的并发写会串行化,不同 key 之间仍可并行。

但原子性是"按方法、按单 key"的——把多个原子方法拼起来的复合逻辑,整体并不原子。最常见的误区是 check-then-act(先查再改):

java
// ❌ 非原子:containsKey 和 put 是两个独立的原子调用,中间可能被其他线程插入
if (!map.containsKey(key)) {
    map.put(key, value);
}
线程 A                          线程 B
containsKey("k") → false
                                containsKey("k") → false
put("k", vA)
                                put("k", vB)   ← 覆盖 A,A 的写入丢失
结果:{k: vB}(A 却以为自己写成功了)

✅ 解法:改用"本身就完成 查+改 的一步式原子方法",把两步并成一步:

java
map.putIfAbsent(key, value);                // 不存在才放入
map.computeIfAbsent(key, k -> newValue());  // 不存在才计算并放入
map.merge(key, 1, Integer::sum);            // 按单 key 累加/聚合
线程 A                          线程 B
putIfAbsent("k", vA)  ← 锁住 k 的桶,"查+写"一步完成
                                putIfAbsent("k", vB) ← 等锁 → 发现已存在 → 放弃
结果:{k: vA}(确定,无丢失)

两个边界:

  • 跨 key 不原子:原子性只作用于单个 key(锁对应的桶)。同时改两个 key、或"遍历所有 key 做统计"这类跨 key 逻辑不具备整体原子性,需要外部加锁或其他同步手段。

    典型例子是"转账"——从 A 扣款、给 B 入账是两次各自原子的操作,但合起来不原子,中间状态会被其他线程看到:

    java
    // ❌ 跨 key 复合操作:扣 A + 加 B 不是一个整体
    balances.merge(from, -amount, Integer::sum); // 第 1 步:A 扣款(本身原子)
    balances.merge(to,   amount, Integer::sum);  // 第 2 步:B 入账(本身原子)
    初始:A=100, B=100(总额应为 200)
    线程 T1(A→B 转 100)                 线程 T2(此刻读总额)
    merge(A, -100) → A=0
                                         sum = A + B = 0 + 100 = 100 ❌ 少了 100
    merge(B, +100) → B=200
                                         sum = A + B = 0 + 200 = 200 ✅

    在第 1、2 步之间,钱已从 A 扣除却还没进 B,此刻读到的总额凭空少一份。✅ ConcurrentHashMap 自身给不了"两个 key 一起改"的原子性,需要对这段逻辑加同一把外部锁(如 synchronized),或改用支持多键原子/事务的结构。

  • 回调里别做重活:compute / computeIfAbsent / merge 的回调执行期间持有桶锁——不要做慢 IO、不要递归更新同一个 map、不要依赖复杂副作用,否则拖慢并发甚至死锁。

基本操作 ​

java
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key1", "value1");
String value = map.get("key1");
map.remove("key1");

条件写入(putIfAbsent / replace) ​

"满足条件才写"的一步式原子方法,正是替代上文 check-then-act 的正解:

java
// putIfAbsent:key 不存在时才放入
map.putIfAbsent("key1", "value1");

// replace:key 存在时才替换
map.replace("key1", "newValue");
map.replace("key1", "oldValue", "newValue"); // 只有旧值匹配才替换(CAS 语义)

计算操作 ​

java
// compute:总是执行计算(无论 key 是否存在)
map.compute("counter", (k, v) -> {
    return v == null ? 1 : v + 1; // 值不存在置 1,否则加 1
});

// computeIfAbsent:key 不存在时才计算(典型场景:懒加载缓存)
map.computeIfAbsent("user:1001", key -> userService.findFromDB(key));

// computeIfPresent:key 存在时才计算(典型场景:更新已有值)
map.computeIfPresent("key1", (k, v) -> v + "_updated");

合并操作 ​

java
// merge:key 不存在直接放入,已存在则合并(典型场景:计数、累加)
map.merge("counter", 1, Integer::sum); // 不存在置 1,已存在则 +1

迭代器弱一致(fail-safe) ​

ConcurrentHashMap 的迭代器是弱一致的:不抛 ConcurrentModificationException,但只反映"创建时某个时间点"的状态,遍历期间的并发写入不保证可见;size() / containsValue() 在并发下也是近似值。适合"大致浏览",不适合"精确遍历"。

java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.put("b", 2);

// ✅ 边遍历边改,不会抛 CME(换成 HashMap,同样的循环会立即抛 ConcurrentModificationException)
for (String key : map.keySet()) {
    if (key.equals("a")) {
        map.put("c", 3);   // 遍历途中新增一个 key
    }
    System.out.println(key);
}
// 本次输出是否包含 "c" 不确定:可能是 [a, b],也可能是 [a, b, c] 或 [a, c, b]

为什么:迭代器基于创建时的桶状态快照向前推进,遍历途中新增的 c 可能落在已扫过的桶里(看不到),也可能落在未扫的桶里(看得到)——这就是"弱一致":不报错,但也不保证看到最新写入。

对比:HashMap 的迭代器是 fail-fast 的,同样的"遍历中修改"会立刻抛 ConcurrentModificationException;ConcurrentHashMap 用弱一致换来了"遍历不被并发修改打断",代价是遍历结果只是近似快照,不能依赖它做精确统计(此时 size() 同样是近似值)。

ConcurrentSkipListMap ​

需要线程安全 + 按 key 排序时用 ConcurrentSkipListMap:基于跳表实现,并发读写仍保持 key 有序,O(log n),相当于 TreeMap 的并发版。同样实现 NavigableMap,支持 subMap/headMap/tailMap/floorKey/ceilingKey 等范围查询。

java
import java.util.concurrent.ConcurrentSkipListMap;

// 并发 + 排序:如实时排行榜(score → playerId)、按时间戳的范围索引
ConcurrentSkipListMap<Long, String> timeline = new ConcurrentSkipListMap<>();
timeline.put(ts, eventId);   // 线程安全,且按 ts 排序
timeline.firstKey();         // 最早时间戳
timeline.subMap(t1, t2);     // [t1, t2) 范围内的事件(范围查询)

选择提示:并发但无需排序 → ConcurrentHashMap;并发且要按 key 排序/范围查询 → ConcurrentSkipListMap。

不可变 Map ​

创建方式说明
Map.of(k1, v1, k2, v2, ...)直接创建不可变 Map(最多 10 组键值对),键值不允许 null
Map.ofEntries(Map.entry(k, v), ...)超过 10 组键值对时使用
Map.copyOf(map)从已有 Map 复制出不可变副本
java
Map<String, Integer> config = Map.of("timeout", 30, "retry", 3);
// config.put("timeout", 60); // UnsupportedOperationException

// 每个 Map.entry 打包一组键值对:
// Map.entry("a", 1) → "a"=1
// Map.entry("b", 2) → "b"=2
// ofEntries 把它们合成一个 Map
Map<String, Integer> big = Map.ofEntries(
    Map.entry("a", 1),
    Map.entry("b", 2));
// big = {a=1, b=2}

Queue ​

ArrayDeque ​

ArrayDeque 是基于循环数组实现的双端队列,可作栈可作队列。它不是线程安全的,且不允许 null 元素。

栈操作(后进先出) ​

方法说明
push(E e)压栈(添加到头部)
pop()弹栈(移除并返回头部)
peek()查看栈顶(不移除)

队列操作(先进先出) ​

方法说明
offer(E e)入队(添加到尾部)
poll()出队(移除并返回头部)
peek()查看队首(不移除)

双端队列操作(两端均可操作) ​

方法说明
offerFirst(E e) / offerLast(E e)在头部 / 尾部添加
pollFirst() / pollLast()移除并返回头部 / 尾部元素
peekFirst() / peekLast()查看头部 / 尾部元素
java
import java.util.ArrayDeque;
import java.util.Deque;

// 当栈用:后进先出
Deque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.push("B");
System.out.println(stack.pop()); // "B"

// 当队列用:先进先出
Deque<String> queue = new ArrayDeque<>();
queue.offer("task-1");
queue.offer("task-2");
System.out.println(queue.poll()); // "task-1"

LinkedBlockingQueue ​

LinkedBlockingQueue 是一个线程安全的队列。容量默认是 Integer.MAX_VALUE,因此通常可以认为它是"无界"的,但需根据环境提供合理限制,否则容易造成内存泄露。

内部使用两把独立的 ReentrantLock——putLock(入队锁)与 takeLock(出队锁)——来提高并发度:生产者与消费者可以一定程度并行,但多个生产者之间仍竞争 putLock、多个消费者之间仍竞争 takeLock。

常用API与阻塞语义 ​

同一件事(入队/出队),BlockingQueue 提供了四种"满/空时怎么办"的语义,选错会导致线程卡死或丢任务:

方法队列满/空时的行为返回/抛出
put(E e) / take()一直阻塞,直到有空位/有元素被中断抛 InterruptedException
offer(E e) / poll()立即返回,不阻塞满返回 false / 空返回 null
offer(E e, timeout, unit) / poll(timeout, unit)最多等待 timeout,超时放弃超时返回 false / null
add(E e)满时抛异常IllegalStateException
size()返回当前元素个数int(并发下非精确)
remainingCapacity()返回剩余容量int
  • 生产者-消费者:用 put/take,让线程在满时自然等待;
  • 需要快速失败或降级:用 offer/poll(可带超时),拿不到就走兜底逻辑,不要阻塞主流程。

生产者-消费者模型示例 ​

java
import java.util.concurrent.LinkedBlockingQueue;

LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(3);

// 生产者
new Thread(() -> {
    try {
        for (int i = 1; i <= 5; i++) {
            String item = "task-" + i;
            queue.put(item); // 队列满则阻塞等待
            System.out.println("生产者:" + item);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

// 消费者
new Thread(() -> {
    try {
        while (true) {
            String item = queue.take(); // 队列空则阻塞等待
            System.out.println("消费者:" + item);
            Thread.sleep(500); // 模拟消费耗时
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

运行结果:

生产者:task-1
生产者:task-2
生产者:task-3 // 队列已满,生产者阻塞
消费者:task-1
生产者:task-4 // 消费后队列有空位,生产者继续
消费者:task-2

容量、最大线程数与拒绝策略 ​

阻塞队列的容量不是孤立参数——它和线程池的 corePoolSize、maximumPoolSize、RejectedExecutionHandler 一起决定系统过载时的行为。ThreadPoolExecutor 提交任务的决策流:

新任务
  │
  ▼
线程数 < corePoolSize ? ──是──▶ 新建线程执行
  │否
  ▼
入队成功(队列未满)? ──是──▶ 排队等待
  │否(队列满)
  ▼
线程数 < maximumPoolSize ? ──是──▶ 新建线程执行
  │否
  ▼
触发拒绝策略 RejectedExecutionHandler
  ├─ AbortPolicy(默认):抛 RejectedExecutionException
  ├─ CallerRunsPolicy:由提交任务的线程自己执行(形成背压)
  ├─ DiscardPolicy:静默丢弃
  └─ DiscardOldestPolicy:丢弃队首最老任务,再尝试提交

关键结论:LinkedBlockingQueue 默认容量是 Integer.MAX_VALUE(近似无界)。无界队列会让"入队"永远成功,于是 maximumPoolSize 形同虚设、线程数停在 core,任务无限堆积最终 OOM。因此生产环境务必给队列设置合理容量,并配合 CallerRunsPolicy 等拒绝策略形成背压。

线程池各参数(core/max/keepAlive/线程工厂)与在 Spring Boot 中的配置方式,见 Java 多线程基础 与 Spring Boot 多线程指南,本文不展开。


枚举专用集合 ​

当 key(或元素)是枚举时,优先用 EnumMap/EnumSet:底层用数组(EnumMap)或位向量(EnumSet)实现,比 HashMap/HashSet 更快、更省内存,迭代顺序即枚举声明顺序。

EnumMap 常用 API:

方法说明
new EnumMap<>(Class<K> keyType)用枚举 Class 创建(key 类型在构造时固定)
put(K key, V value) / get(K key)放入 / 取出
remove(K key) / containsKey(K key)移除 / 判断 key 是否存在
size() / isEmpty() / clear()元素个数 / 是否为空 / 清空

EnumMap 的 key 不允许 null,但 value 允许 null。迭代顺序即枚举声明顺序。

EnumSet 常用 API:

方法说明
EnumSet.of(E... elements)创建包含指定元素的集合
EnumSet.allOf(Class<E> elementType)包含所有枚举值的集合
EnumSet.noneOf(Class<E> elementType)空集合
EnumSet.range(E from, E to)声明顺序区间 [from, to](含两端)
EnumSet.complementOf(EnumSet<E> s)补集:所有不在 s 中的枚举值
EnumSet.copyOf(Collection<E> c)从集合复制出 EnumSet
add(E) / remove(E) / contains(E)标准 Set 操作

EnumSet 的位向量实现使得 contains、add、remove 等操作为 O(1),且内存占用极小(每个枚举值占 1 bit)。不允许 null 元素。

java
enum Status { NEW, DOING, DONE }

// EnumMap:创建时用 Class 固定 key 类型,key 不允许 null
Map<Status, Integer> countByStatus = new EnumMap<>(Status.class);
countByStatus.put(Status.NEW, 3);
countByStatus.put(Status.DONE, 5);

// EnumSet:状态标志集合,位运算实现,高效
Set<Status> flags = EnumSet.of(Status.NEW, Status.DOING);
Set<Status> all = EnumSet.allOf(Status.class);
Set<Status> range = EnumSet.range(Status.NEW, Status.DONE); // 声明顺序区间

适用:以枚举做分组统计、状态机标志位、权限集合等。key 类型在创建时即固定,且不允许 null key。


集合工具类 ​

Collections 提供了一系列静态方法,用于对各种集合进行通用操作。它不是集合本身,而是"集合的瑞士军刀"。

工厂方法 ​

创建特定用途的集合,常用于方法返回值(避免返回 null)或常量定义。

方法说明典型用途
emptyList() / emptySet() / emptyMap()返回空集合(不可变)方法无结果时返回,替代 null
singletonList(e) / singleton(e) / singletonMap(k, v)返回单元素不可变集合需要只包含一个元素的常量集合
nCopies(n, elem)返回 n 个相同元素的不可变列表初始化固定数量的默认值
java
// 空集合:替代返回 null,避免调用方 NPE
public List<User> findUsers(String query) {
    if (query == null || query.isEmpty()) {
        return Collections.emptyList(); // 而不是 null
    }
    // ...
}

// 单元素集合
List<String> onlyOne = Collections.singletonList("admin");
// onlyOne.add("user"); // UnsupportedOperationException

Set<String> singleRole = Collections.singleton("ADMIN");
// singleRole.add("USER"); // UnsupportedOperationException

Map<String, Object> config = Collections.singletonMap("timeout", 30);
// config.put("retry", 3); // UnsupportedOperationException

// n 个相同元素
List<String> defaults = Collections.nCopies(5, "N/A"); // ["N/A", "N/A", "N/A", "N/A", "N/A"]

只读包装 ​

返回原集合的只读视图,任何修改操作(add/remove/set)都会抛 UnsupportedOperationException。

方法说明
unmodifiableList(list)返回 List 的只读视图
unmodifiableSet(set)返回 Set 的只读视图
unmodifiableMap(map)返回 Map 的只读视图
unmodifiableCollection(coll)返回 Collection 的只读视图

与 List.of() 的区别:

  • List.of(...) → 创建真正不可变的集合(底层就是常量,原集合不存在)
  • unmodifiableList(mutableList) → 返回只读视图(底层集合仍可被原引用修改,只是不能通过这个视图修改)
java
List<String> mutable = new ArrayList<>(List.of("A", "B", "C"));
List<String> readOnly = Collections.unmodifiableList(mutable);

// readOnly.add("D"); // UnsupportedOperationException

// 但原集合仍可改,视图会反映变化
mutable.add("D");
System.out.println(readOnly.size()); // 4,视图反映了变化

同步包装 ​

Collections.synchronizedXxx(...) 返回一个线程安全的包装视图:它给每个方法套上同一把锁,使任意单个方法调用互斥。

方法说明
synchronizedList(list)返回线程安全的 List 视图
synchronizedSet(set)返回线程安全的 Set 视图
synchronizedMap(map)返回线程安全的 Map 视图

遍历时必须手动加锁:包装器只保证"单个方法"同步,而一次迭代是多次方法调用的组合,整体并不原子。遍历时若不手动 synchronized (集合) { ... },仍会抛 ConcurrentModificationException。

java
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 遍历必须手动同步在 syncList 上
synchronized (syncList) {
    for (String s : syncList) {
        // ...
    }
}

高并发场景优先用 java.util.concurrent 的并发集合(如 ConcurrentHashMap、CopyOnWriteArrayList),性能远好于"全表一把锁"的同步包装。

操作类方法 ​

功能方法说明
集合操作reverse、shuffle、rotate、swap反转、随机打乱、旋转、交换
查找max、min、binarySearch、frequency最大/最小值、二分查找、出现次数
修改fill、copy、replaceAll填充、复制、替换
java
import java.util.Collections;

List<Integer> list = new ArrayList<>(List.of(3, 1, 2));

// 集合操作
Collections.reverse(list);              // 反转:[2, 1, 3]
Collections.shuffle(list);              // 随机打乱:顺序随机
Collections.swap(list, 0, 2);           // 交换下标 0 和 2

// 查找
int max = Collections.max(list);        // 最大值
int min = Collections.min(list);        // 最小值
int count = Collections.frequency(list, 2); // 元素 2 出现的次数
int index = Collections.binarySearch(list, 2); // 二分查找(需先排序)

// 修改
Collections.fill(list, 0);              // 全部填充为 0
Collections.replaceAll(list, 0, 1);     // 替换所有 0 为 1

Arrays 工具方法 ​

java.util.Arrays 提供了一系列操作数组的静态方法,在数组与集合互转、数组拷贝/比较/填充等场景下非常常用。

数组与集合互转 ​

方法说明注意事项
Arrays.asList(arr)返回数组的定长列表视图改互相影响;不能 add/remove(UOE)
Arrays.stream(arr)返回数组的 Stream可收集为 List/Set 等
list.toArray(new Type[0])集合转数组推荐 new Type[0] 而非 new Type[size()]
new ArrayList<>(Arrays.asList(arr))创建独立的可变列表副本与原数组脱钩
java
String[] arr = {"A", "B", "C"};

// 数组 → 定长列表视图(改互相影响)
List<String> view = Arrays.asList(arr);
view.set(0, "X");        // arr[0] 也变成 "X"
// view.add("D");         // UnsupportedOperationException

// 数组 → 独立可变列表
List<String> mutable = new ArrayList<>(Arrays.asList(arr));
mutable.add("D");         // 可以 add

// 数组 → Stream → 收集为 List
List<String> list = Arrays.stream(arr).collect(Collectors.toList());

// 基本类型数组的坑
int[] nums = {1, 2, 3};
// Arrays.asList(nums) → 得到 List<int[]>,size()==1,不是 List<Integer>!
// 正确做法:
List<Integer> intList = Arrays.stream(nums).boxed().collect(Collectors.toList());

// 集合 → 数组
String[] out = list.toArray(new String[0]); // 推荐写法

数组操作 ​

方法说明
Arrays.copyOf(arr, newLen)拷贝数组,可扩容或截断
Arrays.equals(a, b)比较两个数组内容是否相等(浅比较)
Arrays.deepEquals(a, b)多维数组深比较
Arrays.fill(arr, val)将数组所有元素填充为指定值
Arrays.sort(arr)对数组原地排序
Arrays.sort(arr, comp)自定义排序
Arrays.binarySearch(arr, key)二分查找(需先排序)
java
int[] src = {3, 1, 4, 1, 5};

// 拷贝/扩容
int[] copy = Arrays.copyOf(src, 3);    // [3, 1, 4](截断)
int[] expanded = Arrays.copyOf(src, 8); // [3, 1, 4, 1, 5, 0, 0, 0](扩容补 0)

// 比较
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
Arrays.equals(a, b);          // true

// 填充
int[] filled = new int[5];
Arrays.fill(filled, 7);       // [7, 7, 7, 7, 7]

// 排序
Arrays.sort(src);             // [1, 1, 3, 4, 5]
Arrays.sort(src, Collections.reverseOrder()); // 降序(仅对象数组)

// 二分查找(需先排序)
int idx = Arrays.binarySearch(src, 3); // 找到返回下标,否则返回负数

基本类型数组排序:Arrays.sort(int[]) 使用双轴快排,对对象数组使用 TimSort(稳定)。基本类型降序需手动实现或用包装类型。


排序 ​

Java 8+ 对排序 API 做了大幅简化:list.sort(comparator) 取代了 Collections.sort(),同时 Comparator 引入了一系列静态工厂方法,让多字段排序变得简洁。

list.sort vs Collections.sort ​

维度Collections.sort(list, comp)list.sort(comp)
引入版本Java 1.2Java 8
推荐程度可用但冗余推荐
内部实现Java 8+ 委托给 list.sort()TimSort
java
// ❌ 旧写法
Collections.sort(list, String::compareTo);

// ✅ 新写法
list.sort(String::compareTo);
list.sort(Comparator.naturalOrder());
list.sort(Comparator.reverseOrder());

Comparator 静态工厂方法 ​

Comparator(比较器) 是一个函数式接口,定义了两个对象的排序顺序:int compare(T o1, T o2),返回负数 / 零 / 正数表示 o1 < o2 / o1 == o2 / o1 > o2。

传统写法需要自己实现比较逻辑:

java
// 传统 lambda 写法:按年龄排序
users.sort((a, b) -> Integer.compare(a.getAge(), b.getAge()));

// 传统写法:按姓名排序
users.sort((a, b) -> a.getName().compareTo(b.getName()));

Java 8 引入静态工厂方法后,排序变得声明式:你只需告诉系统"按什么字段排序",不用关心底层比较逻辑。

核心思想:Comparator.comparing(keyExtractor) = 提取 key,按 key 排序。

java
// 按年龄排序:提取 age 字段,按 age 排序
users.sort(Comparator.comparing(User::age));

// 按姓名排序:提取 name 字段,按 name 排序
users.sort(Comparator.comparing(User::name));

comparingInt / comparingLong / comparingDouble 是基本类型特化版本,避免装箱(int → Integer),性能更好。当字段是基本类型时优先使用。

这是 Java 8 引入的排序核心 API,企业开发高频使用:

方法说明
Comparator.comparing(keyExtractor)按某字段排序
Comparator.comparingInt/Long/Double(extractor)按基本类型字段排序(避免装箱,性能更好)
Comparator.comparing(keyExtractor, keyComparator)按某字段排序,自定义比较器
.thenComparing(...)次要排序字段(多字段排序)
.reversed()反转排序顺序
.nullsFirst(comparator)null 排最前
.nullsLast(comparator)null 排最后
java
record User(String name, int age, String city) {}

List<User> users = List.of(
    new User("Alice", 30, "Beijing"),
    new User("Bob", 25, "Shanghai"),
    new User("Charlie", 30, "Beijing"),
    new User(null, 28, "Shenzhen")
);

// 单字段排序
users.sort(Comparator.comparing(User::age));
// [Bob(25), null(28), Alice(30), Charlie(30)]

// 多字段排序:先按 age,age 相同按 name
users.sort(Comparator.comparing(User::age)
                     .thenComparing(User::name));
// [Bob(25), null(28), Alice(30), Charlie(30)]
// 注意:name 为 null 的排最后(null 默认比任何值大)

// 逆序
users.sort(Comparator.comparing(User::age).reversed());
// [Alice(30), Charlie(30), null(28), Bob(25)]

// null 处理:显式指定 null 排最后
users.sort(Comparator.comparing(User::name,
                                Comparator.nullsLast(String::compareTo)));
// [Alice, Bob, Charlie, null]

// 组合:先按 city,再按 age 降序,name 为 null 排最后
users.sort(Comparator.comparing(User::city)
                     .thenComparing(Comparator.comparing(User::age).reversed())
                     .thenComparing(User::name, Comparator.nullsLast(String::compareTo)));

要点:

  • comparing 提取的字段如果是对象类型且可能为 null,用 comparing(field, Comparator.nullsLast(...)) 显式处理
  • reversed() 是对整个 Comparator 反转,不是对单个字段
  • thenComparing 可链式调用多个,实现任意层级的多字段排序

Stream Collectors 与集合互转 ​

把 List 聚合成 Map / 分组 / 拼接,统一用 Stream 的 Collectors 完成。

速查表、基础示例、进阶用法与常见陷阱(toMap key 冲突、value 为 null 等)见 Lambda 与 Stream API → Collectors 进阶。