
Java集合框架是JDK中使用频率最高的核心类库,也是90%以上Java线上业务Bug的发源地。无论是日常业务开发、架构设计还是面试进阶,对集合框架的源码级理解,都是区分普通开发者和资深工程师的核心分水岭。本文基于JDK 17,从底层源码、架构选型、线程安全三大维度,拆解集合框架的核心逻辑,给出选型法则和避坑指南。
Java集合框架分为两大根体系:Collection单元素集合体系和Map键值对集合体系,所有集合类均基于这两个根接口扩展实现,整体架构如下:

各分支核心职责:
Collection:单元素集合的根接口,定义了所有单元素集合的通用方法,如add、remove、contains、size等List:有序、可重复、支持下标随机访问的集合Set:无序、不可重复的集合Queue/Deque:队列/双端队列,用于实现FIFO/LIFO数据结构Map:键值对集合的根接口,key不可重复,每个key对应唯一valueList的核心特性:元素插入顺序与取出顺序一致、可重复、支持通过下标随机访问,核心实现类包括ArrayList、LinkedList、Vector、CopyOnWriteArrayList。
ArrayList底层基于动态扩容的Object数组实现,是日常开发中使用频率最高的集合类,JDK 17中核心源码特性如下:
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 无参构造使用的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储元素的底层数组,transient修饰避免序列化整个空数组
transient Object[] elementData;
// 集合中实际元素的数量
private int size;
多数开发者误以为无参构造的ArrayList初始容量为10,实际上JDK 17中,无参构造创建的ArrayList,底层数组初始化为空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,仅在第一次调用add方法时,才会触发扩容并将数组初始化为默认容量10,通过懒加载节省内存开销。
当添加元素时,若底层数组剩余容量不足以容纳新元素,会触发grow()方法执行扩容,核心逻辑如下:
oldCapacity + (oldCapacity >> 1),位运算效率远高于除法)Integer.MAX_VALUE - 8,则直接扩容至Integer.MAX_VALUEArrays.copyOf()复制原数组到新容量数组,完成扩容扩容流程如下:

ConcurrentModificationException,甚至出现数据丢失、数组越界LinkedList底层基于双向链表实现,每个元素被封装为Node节点,包含prev前驱指针、item元素值、next后继指针。
// 链表头节点
transient Node<E> first;
// 链表尾节点
transient Node<E> last;
// 链表实际元素数量
transient int size;
多数开发者存在误区:认为LinkedList所有插入/删除操作都是O(1)。实际上:
List和Deque双接口,既可以作为有序列表,也可以作为双端队列、栈使用ArrayDeque替代Vector是JDK 1.0推出的古老线程安全List,所有方法都添加了synchronized全表锁,并发度极低、性能极差,官方已明确不推荐使用。CopyOnWriteArrayList是JUC包提供的线程安全List,核心思想是写时复制:
ReentrantLock独占锁,避免并发复制导致的数据混乱核心特性:
Map是键值对(key-value)集合的根接口,核心特性:key不可重复,每个key对应唯一value,核心实现类包括HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。
HashMap是日常开发中使用最广泛的KV集合,JDK 17中底层采用数组+链表+红黑树的混合结构实现,核心源码逻辑如下:
// 默认初始容量,必须是2的幂
staticfinalint DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 最大容量
staticfinalint MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子
staticfinalfloat DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树的阈值
staticfinalint TREEIFY_THRESHOLD = 8;
// 红黑树转回链表的阈值
staticfinalint UNTREEIFY_THRESHOLD = 6;
// 链表转红黑树的最小数组容量
staticfinalint MIN_TREEIFY_CAPACITY = 64;
// 哈希桶数组,长度必须是2的幂
transient Node<K,V>[] table;
// 实际键值对数量
transientint size;
// 扩容阈值,threshold = 容量 * 负载因子
int threshold;
// 负载因子
finalfloat loadFactor;
HashMap的哈希函数是核心优化点,JDK 17中哈希值计算逻辑为:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
核心设计逻辑:
(n - 1) & hash,等价于hash % n,但位运算效率远高于取模,这也是数组长度必须是2的幂的核心原因put方法是HashMap的核心,完整流程如下:

注:n减1与hash按位与 → (n-1) & hash
当同时满足以下两个条件时,链表会转换为红黑树:
TREEIFY_THRESHOLD(8)MIN_TREEIFY_CAPACITY(64)若仅链表长度达到8,但数组长度小于64,会优先触发扩容而非转红黑树。当红黑树节点数量小于等于UNTREEIFY_THRESHOLD(6)时,会自动转回链表。 核心设计目的:解决哈希碰撞导致的链表过长问题,将查询时间复杂度从O(n)优化到O(logn),提升极端场景下的性能。
当HashMap的元素数量size超过扩容阈值threshold时,会触发resize()扩容,核心逻辑:
LinkedHashMap继承自HashMap,底层在HashMap结构基础上,额外维护了一条双向链表,记录元素的插入顺序或访问顺序,核心特性:
accessOrder设置为访问顺序(LRU缓存的核心实现)TreeMap实现了SortedMap接口,底层基于红黑树实现,核心特性:
headMap、tailMap、subMap,这是HashMap无法实现的ConcurrentHashMap是JUC包提供的高并发线程安全Map,是高并发场景下KV存储的首选。JDK 17中,ConcurrentHashMap放弃了JDK 7的分段锁设计,采用CAS + synchronized实现,锁粒度为哈希桶的首节点,并发度大幅提升。
get(key)返回null时,无法区分是key不存在还是value为null。单线程HashMap可通过containsKey区分,但并发场景下,get和containsKey之间可能有其他线程修改,存在竞态条件,因此禁止null值。putIfAbsent、compute、merge、replace等原子方法,解决复合操作的竞态问题。Set是无序、不可重复的集合,底层几乎全部基于对应的Map实现:
Set的不可重复特性完全依赖Map的key不可重复特性,因此存入Set的元素必须正确重写hashCode()和equals()方法,否则会导致去重失效。
Queue/Deque是用于实现队列、栈的集合接口,核心实现类:
基于源码特性和业务场景,总结出可直接落地的选型黄金法则,覆盖99%的业务场景,如下表所示:
业务场景 | 核心需求 | 推荐集合 | 不推荐集合 | 核心理由 |
|---|---|---|---|---|
单线程读多写少,随机访问频繁 | 高性能随机读,有序可重复 | ArrayList | LinkedList | ArrayList随机访问O(1),LinkedList随机访问O(n),性能差距可达100倍以上 |
单线程频繁首尾插入/删除,无需随机访问 | 高性能首尾操作 | ArrayDeque | LinkedList | ArrayDeque基于循环数组,缓存友好,性能比LinkedList高50%以上 |
高并发读多写少,遍历远多于修改 | 线程安全,读无锁,遍历安全 | CopyOnWriteArrayList | Vector/Collections.synchronizedList | 读操作无锁,性能远超全表锁的Vector,适合配置列表、白名单等读多写少场景 |
单线程KV存储,无有序要求 | 高性能put/get,无同步开销 | HashMap | Hashtable/TreeMap | HashMap无同步开销,O(1)时间复杂度,性能远超有序的TreeMap和全表锁的Hashtable |
单线程KV存储,需要保证插入顺序 | 插入顺序有序,高性能 | LinkedHashMap | TreeMap | LinkedHashMap仅额外维护双向链表,性能接近HashMap,远高于O(logn)的TreeMap |
单线程KV存储,需要按key排序/范围查询 | 排序能力,范围查询 | TreeMap | HashMap/LinkedHashMap | TreeMap基于红黑树实现排序,原生支持范围查询,是唯一支持有序范围查询的通用Map |
高并发KV存储,线程安全 | 高并发读写,原子操作支持 | ConcurrentHashMap | Collections.synchronizedMap/Hashtable | 细粒度锁+CAS,并发度远高于全表锁的同步Map,提供丰富的原子操作方法 |
单线程去重,无有序要求 | 高性能去重 | HashSet | TreeSet | HashSet基于HashMap实现,O(1)时间复杂度,性能远超O(logn)的TreeSet |
高并发去重,读多写少 | 线程安全去重,读无锁 | CopyOnWriteArraySet | Collections.synchronizedSet | 基于CopyOnWriteArrayList实现,读无锁,适合读多写少的并发去重场景 |
集合相关的线上Bug,90%都来自于线程安全问题,本节拆解最常见的核心坑点,给出错误示例、问题根源和正确解决方案,所有示例均可直接运行验证。
ArrayList的迭代器遍历过程中,若通过集合自身的add/remove方法修改集合,会触发modCount(修改次数)和迭代器的expectedModCount不一致,抛出并发修改异常。多线程并发遍历和修改时,即使单线程内操作正确,也会触发该异常。
package com.jam.demo.collection.pitfall;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* ArrayList并发修改异常错误示例
* @author ken
*/
@Slf4j
publicclass ArrayListModificationPitfall {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add("元素" + i);
}
// 错误用法:迭代器遍历中用集合的remove方法
try {
for (String s : list) {
if ("元素5".equals(s)) {
list.remove(s);
}
}
} catch (Exception e) {
log.error("单线程迭代器修改触发并发修改异常", e);
}
}
}
remove()方法,而非集合自身的方法package com.jam.demo.collection.pitfall;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* ArrayList并发修改异常正确示例
* @author ken
*/
@Slf4j
publicclass ArrayListModificationCorrect {
public static void main(String[] args) {
// 单线程正确用法:使用迭代器的remove方法
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add("元素" + i);
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("元素5".equals(s)) {
iterator.remove();
}
}
log.info("单线程正确操作后集合:{}", list);
// 多线程正确用法:使用CopyOnWriteArrayList
CopyOnWriteArrayList<String> concurrentList = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
concurrentList.add("元素" + i);
}
// 遍历过程中修改不会触发异常
for (String s : concurrentList) {
if ("元素3".equals(s)) {
concurrentList.remove(s);
}
}
log.info("多线程正确操作后集合:{}", concurrentList);
}
}
Collections.synchronizedList/synchronizedMap的所有方法都加了synchronized锁,但迭代器方法没有加锁,并发遍历和修改时,依然会触发并发修改异常,这是最常见的误区之一。
package com.jam.demo.collection.pitfall;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 同步包装类迭代器线程不安全错误示例
* @author ken
*/
@Slf4j
publicclass SynchronizedListPitfall {
privatestaticfinal List<String> SYNC_LIST = Collections.synchronizedList(new ArrayList<>());
privatestaticfinal CountDownLatch LATCH = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
SYNC_LIST.add("元素" + i);
}
// 线程1:遍历集合
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> {
try {
for (String s : SYNC_LIST) {
// 模拟业务处理
Thread.sleep(1);
}
} catch (Exception e) {
log.error("同步包装类遍历触发并发修改异常", e);
} finally {
LATCH.countDown();
}
});
// 线程2:修改集合
executor.execute(() -> {
try {
for (int i = 0; i < 100; i++) {
SYNC_LIST.add("新增元素" + i);
Thread.sleep(10);
}
} finally {
LATCH.countDown();
}
});
LATCH.await();
executor.shutdown();
}
}
遍历同步包装类时,必须手动对集合对象加锁,确保遍历和修改的原子性:
// 正确用法:遍历过程中对集合对象加锁
synchronized (SYNC_LIST) {
for (String s : SYNC_LIST) {
// 业务处理
}
}
ConcurrentHashMap的单个put/get方法是原子的,但if (!map.containsKey(key)) { map.put(key, value); }这类复合操作,不是原子的,containsKey和put之间可能有其他线程插入,导致数据覆盖、计数错误等问题。
package com.jam.demo.collection.pitfall;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ConcurrentHashMap复合操作非原子性示例
* @author ken
*/
@Slf4j
publicclass ConcurrentHashMapAtomicPitfall {
privatestaticfinal ConcurrentHashMap<String, Integer> COUNT_MAP = new ConcurrentHashMap<>();
privatestaticfinalint THREAD_COUNT = 100;
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch wrongLatch = new CountDownLatch(THREAD_COUNT);
// 错误用法:get+put复合操作非原子
for (int i = 0; i < THREAD_COUNT; i++) {
executor.execute(() -> {
try {
wrongAddCount("test");
} finally {
wrongLatch.countDown();
}
});
}
wrongLatch.await();
log.info("错误用法最终计数:{},预期值:{}", COUNT_MAP.get("test"), THREAD_COUNT);
// 重置
COUNT_MAP.clear();
CountDownLatch correctLatch = new CountDownLatch(THREAD_COUNT);
// 正确用法:使用原子方法merge
for (int i = 0; i < THREAD_COUNT; i++) {
executor.execute(() -> {
try {
correctAddCount("test");
} finally {
correctLatch.countDown();
}
});
}
correctLatch.await();
log.info("正确用法最终计数:{},预期值:{}", COUNT_MAP.get("test"), THREAD_COUNT);
executor.shutdown();
}
/**
* 错误的计数累加方法,get+put非原子操作
* @param key 计数key
*/
private static void wrongAddCount(String key) {
Integer count = COUNT_MAP.get(key);
count = count == null ? 1 : count + 1;
COUNT_MAP.put(key, count);
}
/**
* 正确的计数累加方法,使用原子方法merge
* @param key 计数key
*/
private static void correctAddCount(String key) {
COUNT_MAP.merge(key, 1, Integer::sum);
}
}
运行结果:错误用法的最终计数几乎永远小于100,而正确用法的计数永远等于100,完美验证了复合操作的非原子性。
CopyOnWriteArrayList的写操作会复制整个底层数组,数组越大,写操作的内存开销和CPU开销越大,频繁写会导致大量数组对象创建,触发频繁Full GC,甚至OOM。
CopyOnWriteArrayList仅适合读多写少、遍历远多于修改的场景,比如配置列表、白名单、黑名单等几乎不修改的场景,绝对不能用于写操作频繁的业务场景,比如日志收集、数据统计等。
Arrays.asList返回的是Arrays的内部类ArrayList,而非java.util.ArrayList,该内部类没有重写add/remove方法,直接继承了AbstractList的默认实现,会抛出UnsupportedOperationException。
package com.jam.demo.collection.pitfall;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Arrays.asList坑示例
* @author ken
*/
@Slf4j
publicclass ArraysAsListPitfall {
public static void main(String[] args) {
// 错误用法
List<String> wrongList = Arrays.asList("a", "b", "c");
try {
wrongList.add("d");
} catch (UnsupportedOperationException e) {
log.error("Arrays.asList生成的List不支持add操作", e);
}
// 正确用法:转换为java.util.ArrayList
List<String> correctList = new ArrayList<>(Arrays.asList("a", "b", "c"));
correctList.add("d");
log.info("正确转换后的List操作成功,当前元素:{}", correctList);
}
}
很多开发者用if (list.size() == 0)判断集合为空,若list为null,会直接抛出NullPointerException。阿里巴巴Java开发手册明确规定:集合判空必须使用集合工具类。
使用Spring框架的CollectionUtils工具类进行集合判空:
// 正确判空:同时判断null和空集合
if (CollectionUtils.isEmpty(list)) {
log.info("集合为空");
}
// 正确判断非空
if (CollectionUtils.isNotEmpty(list)) {
log.info("集合非空");
}
同理,字符串判空使用StringUtils.hasText(),对象判空使用ObjectUtils.isEmpty()。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jam</groupId>
<artifactId>collection-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>6.1.5</spring.version>
<lombok.version>1.18.34</lombok.version>
<guava.version>33.1.0-jre</guava.version>
<fastjson2.version>2.0.52</fastjson2.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<mysql.version>8.4.0</mysql.version>
<swagger.version>2.5.0</swagger.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.2.5</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>