吃透集合通关面试|Java 集合 + Stream 生产实战真题全集
吃透集合通关面试|Java 集合 + Stream 生产实战真题全集
基于阿里云、腾讯云、华为云官方技术文档及大厂面试真题整理,涵盖基础概念、List、Set、Map、并发集合、队列、迭代器、性能选型、高频坑点、生产实战全模块,适配初/中/高级Java面试,附带标准答题模板、底层原理、易错点解析。
一、基础概念篇(入门必考)
1. Java集合框架的整体结构是什么?
Java集合框架核心分为**Collection(单元素集合)和Map(键值对集合)**两大顶层体系,同时包含工具类、迭代器、并发集合拓展体系,整体架构清晰、职责分明:
Collection(存储单个元素,可迭代)
├── List(有序、可重复、支持索引)
│ ├── ArrayList (动态数组,主流常用)
│ ├── LinkedList (双向链表)
│ └── Vector (线程安全数组,遗留类)
├── Set(无序/有序、不可重复、无索引)
│ ├── HashSet (哈希去重,无序)
│ ├── LinkedHashSet (保留插入顺序)
│ └── TreeSet (自然/自定义排序)
└── Queue/Deque(队列,用于存取、排队、任务调度)
├── PriorityQueue (优先级队列)
├── ArrayDeque (数组双端队列)
└── 并发队列(ConcurrentLinkedQueue等)
Map(存储Key-Value键值对,无迭代能力,独立体系)
├── HashMap (哈希映射,无序,主流)
├── LinkedHashMap (保留插入/访问顺序,支持LRU)
├── TreeMap (Key有序,红黑树实现)
├── Hashtable (线程安全,遗留类)
└── ConcurrentHashMap (高并发线程安全Map)
2. Collection 和 Collections 的区别?
| 对比项 | Collection | Collections |
|---|---|---|
| 类型 | 顶层接口 | 工具类(全静态方法) |
| 作用 | 定义List、Set、Queue通用规范,包含集合增删改查、遍历核心方法 | 提供集合排序、同步、空集合、不可变集合等工具方法 |
| 实例化 | 无法直接实例化,需子类实现 | 无需实例化,直接调用静态方法 |
| 常用示例 | List、Set、Queue均继承该接口 | Collections.sort()、Collections.synchronizedList()、Collections.emptyList() |
3. List、Set、Map 的核心区别?
| 特性 | List | Set | Map |
|---|---|---|---|
| 存储结构 | 单个元素存储 | 单个元素存储 | Key-Value键值对存储 |
| 有序性 | 严格有序(插入顺序) | 无序(LinkedHashSet保留插入序、TreeSet排序序) | 无序(LinkedHashMap有序、TreeMap排序序) |
| 元素重复性 | 允许元素重复 | 元素绝对不可重复 | Key不可重复,Value可重复 |
| 查询方式 | 下标索引精准查询 | 哈希/比较器匹配查询 | 通过Key唯一查询Value |
| Null值支持 | 允许多个null元素 | HashSet/LinkedHashSet允许1个null,TreeSet不允许 | HashMap允许1个null Key、多个null Value |
| 遍历方式 | 普通for、增强for、迭代器 | 增强for、迭代器 | entrySet、keySet、values遍历 |
4. 为什么 Map 接口不继承 Collection 接口?
核心是设计理念和数据模型不匹配,强行继承会破坏集合框架的规范性:
-
数据模型不同:Collection存储的是单个独立元素,而Map存储的是Key-Value键值对二元数据,数据结构本质不同;
-
核心方法不兼容:Collection的add()、contains()等方法针对单元素设计,无法适配Map的键值对操作逻辑;
-
职责隔离:Map是独立的映射体系,专注键值映射、快速查找,Collection专注单元素批量操作,解耦设计更合理;
-
拓展性更强:独立体系可针对性实现哈希、红黑树等映射结构,无需兼容单元素集合的约束。
二、List 接口高频面试题(核心必考)
5. ArrayList 和 LinkedList 的深度区别?
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层数据结构 | 动态扩容Object数组 | 双向循环链表(JDK1.6+) |
| 随机访问 | O(1),直接通过下标索引访问 | O(n),需从头/尾遍历定位元素 |
| 尾部增删 | O(1)(无扩容时),扩容O(n) | O(1),仅修改首尾指针 |
| 中间增删 | O(n),需批量移动后续元素 | O(n)(耗时在定位),修改指针O(1) |
| 内存占用 | 较小,仅存储元素,无额外开销,存在数组闲置空间 | 较大,每个节点存储元素+前驱/后继双指针 |
| 线程安全 | 非线程安全 | 非线程安全 |
| 遍历效率 | 普通for循环最快,迭代器次之 | 迭代器遍历快,普通for循环极慢(O(n²)) |
| 适用场景 | 绝大多数业务(查询多、增删少) | 频繁中间插入/删除、少量查询场景 |
| 面试选型总结:95%的业务场景优先用ArrayList,仅高频中间增删场景选用LinkedList。 |
6. ArrayList 完整扩容机制(JDK1.8 核心)
ArrayList默认采用懒加载机制,初始化不创建数组,首次添加元素才初始化容量,核心规则如下:
核心参数:
-
初始空数组:
DEFAULTCAPACITY_EMPTY_ELEMENTDATA(容量0) -
首次扩容容量:10
-
扩容倍数:1.5倍(oldCapacity + oldCapacity >> 1)
-
负载因子:1(数组满了才触发扩容)
完整扩容流程:
-
调用add()方法,先校验数组容量,判断是否需要扩容;
-
若为空数组,首次扩容为10;
-
非空数组,计算新容量=原容量1.5倍;
-
若1.5倍容量仍不足,直接使用所需最小容量;
-
通过
Arrays.copyOf()复制原数组元素到新数组,完成扩容。
// JDK1.8 核心扩容源码
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 核心:1.5倍扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 1.5倍不足则直接使用最小所需容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 数组拷贝,耗时O(n)
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容缺点:扩容伴随数组拷贝,频繁扩容会损耗性能,初始化可预估容量提前指定,减少扩容次数。
7. ArrayList 为什么线程不安全?会出现什么问题?
核心原因:新增元素操作非原子性,无锁保护,多线程并发操作会出现数据覆盖、元素丢失、size错乱问题。
// ArrayList 核心不安全源码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 校验容量
elementData[size++] = e; // 非原子操作:赋值+自增两步
return true;
}
并发不安全场景:
-
元素覆盖丢失:两个线程同时获取同一个size下标,先后赋值,后赋值的覆盖先赋值的元素;
-
size计数错乱:两个线程同时完成赋值,size只自增一次,出现size大于实际元素数量;
-
数组越界异常:并发扩容时,多线程同时修改数组容量,触发下标越界。
解决方案:并发场景使用CopyOnWriteArrayList,不使用Vector(性能太差)。
8. Vector 和 ArrayList 的核心区别?
| 对比项 | Vector | ArrayList |
|---|---|---|
| 线程安全 | 安全,所有方法加synchronized全局锁 |
非线程安全,无锁 |
| 性能 | 极低,全局锁并发阻塞严重 | 高性能,无锁开销 |
| 扩容倍数 | 固定2倍扩容 | 1.5倍扩容,内存利用率更高 |
| 诞生版本 | JDK1.0(早期遗留类) | JDK1.2(全新集合框架) |
| 使用现状 | 完全不推荐生产使用 | 通用首选List集合 |
9. CopyOnWriteArrayList 原理、优缺点及适用场景?
核心原理:写时复制(COW)
读操作完全无锁,直接读取原数组;写操作(增删改)加可重入锁,先复制一份新数组,在新数组上完成修改,修改完成后替换原数组,实现读写分离。
// 核心add源码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 复制新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 替换原数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
优点:
-
读写分离,读操作极高并发、无阻塞;
-
线程安全,适配多线程场景;
-
遍历不抛并发修改异常(弱一致性)。
缺点:
-
写操作开销极大,每次修改都需复制数组,占用内存、消耗CPU;
-
数据弱一致性,读操作可能读取到旧数据;
-
不适合大批量写操作场景。
适用场景:读多写少(系统配置、白名单、常量列表、本地缓存)。
三、Set 接口高频面试题
10. HashSet 底层实现原理?
HashSet 底层完全基于 HashMap 实现,是 HashMap 的简化封装:
-
HashSet存储的元素作为HashMap的Key;
-
HashMap的Value固定为一个静态常量空对象
PRESENT,无实际意义;
public class HashSet<E> {
// 底层依赖HashMap
private transient HashMap<E, Object> map;
// 固定Value常量
private static final Object PRESENT = new Object();
// 添加元素本质是HashMap put操作
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}
11. HashSet 如何保证元素唯一性(不重复)?
通过hashCode() + equals() 双重校验实现去重,完整判断流程:
-
新增元素时,先调用对象的
hashCode()计算哈希值,定位数组桶下标; -
若对应桶位置无元素:直接存入,判定为新元素;
-
若桶位置已有元素:调用
equals()逐个比较元素内容; -
equals() == true:判定为重复元素,不存储; -
equals() == false:判定为哈希碰撞,追加到链表/红黑树中。
核心考点:重写实体类时,必须同时重写 hashCode() 和 equals(),否则会出现「内容相同但哈希值不同」,导致去重失效。
12. HashSet、LinkedHashSet、TreeSet 三者深度对比?
| 对比项 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层结构 | HashMap(数组+链表+红黑树) | LinkedHashMap | TreeMap(纯红黑树) |
| 元素顺序 | 完全无序 | 保留插入顺序 | 自然排序/自定义排序 |
| Null值支持 | 允许1个null | 允许1个null | 不允许null(无法比较) |
| 查询性能 | O(1) 极致高效 | O(1) 略低于HashSet | O(logn) 排序损耗 |
| 去重依据 | hashCode+equals | hashCode+equals | compareTo/compare返回0 |
| 适用场景 | 单纯去重、无需有序 | 去重+保留插入顺序 | 去重+自动排序 |
13. TreeSet 两种定制排序方式?
TreeSet基于红黑树排序,必须指定排序规则,支持自然排序和自定义比较器排序两种方式:
方式1:实体类实现 Comparable 接口(自然排序)
public class Person implements Comparable<Person> {
private Integer age;
// 重写比较方法,定义排序规则
@Override
public int compareTo(Person o) {
return this.age - o.age; // 升序;反之降序
}
}
方式2:传入 Comparator 比较器(临时自定义排序,优先级更高)
// Lambda表达式实现降序排序
TreeSet<Person> set = new TreeSet<>((p1, p2) -> p2.getAge() - p1.getAge());
注意:TreeSet判定元素重复的依据是compare返回0,而非equals/hashCode,这是高频易错点。
四、Map 接口核心面试题(重中之重)
14. HashMap JDK1.7与1.8底层结构区别?
| JDK版本 | 底层数据结构 | 核心优化 |
|---|---|---|
| JDK1.7 | 数组 + 单向链表 | 无红黑树,哈希冲突严重时链表过长,查询退化O(n) |
| JDK1.8+ | 数组 + 单向链表 + 红黑树 | 长链表转红黑树,查询效率优化为O(logn) |
| 树化/链表还原阈值(高频考点): |
-
链表长度**>8** 且 数组容量≥64:链表转为红黑树;
-
仅链表长度>8但数组容量<64:优先扩容,不树化;
-
红黑树节点数**<6**:红黑树退化为链表(平衡性能与内存)。
15. HashMap 完整 put 流程(面试满分答案)
-
判断数组是否为空/长度为0,无则执行首次扩容初始化容量为16;
-
通过哈希算法计算key的hash值:
hash = key == null ? 0 : h ^ h>>>16; -
通过位运算计算数组下标:
index = (table.length - 1) & hash; -
若对应下标桶为空,直接新建节点存入键值对,结束流程;
-
若桶不为空,判断桶首节点key与当前key是否一致(hash相同+equals为true),一致则覆盖旧value;
-
若key不一致,判断桶结构是红黑树还是链表:
-
红黑树:执行红黑树插入逻辑;
-
链表:遍历链表查找重复key,有则覆盖,无则尾部插入;
-
-
插入完成后,判断链表长度是否达到树化阈值,满足则转为红黑树;
-
最终判断元素数量是否超过阈值(容量*0.75),超阈值则触发扩容。
16. HashMap 哈希函数为什么要高低16位异或?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
核心设计目的:
HashMap数组长度默认是2的n次幂,位运算取模只会用到哈希值的低16位,高位完全不参与计算;若哈希值差异仅在高位,会导致大量key哈希冲突、扎堆存储。
将高16位与低16位异或,让高位特征融入低位,让哈希值分布更均匀,极大减少哈希冲突概率,提升查询效率。
17. HashMap 扩容机制及JDK1.8优化点?
核心参数:初始容量16,负载因子0.75,扩容倍数2倍,扩容阈值=容量*0.75
扩容触发条件:集合元素个数size > 扩容阈值threshold
JDK1.8扩容核心优化(高频面试):
JDK1.7扩容需要重新遍历所有元素、重新计算哈希下标,效率极低;
JDK1.8优化:扩容后元素下标只有两种可能:原位置 或 原位置+旧容量,无需重新完整计算哈希,大幅提升扩容效率。
18. HashMap 和 Hashtable 全方位对比?
| 对比项 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(全局synchronized锁) |
| Null键值支持 | 允许1个null Key、多个null Value | 不允许null Key/Value,直接抛空指针 |
| 初始容量 | 16(2的幂次) | 11(非2的幂次) |
| 扩容规则 | 2倍扩容 | 2倍+1扩容 |
| 迭代器特性 | fail-fast 快速失败 | 非fail-fast |
| 性能与现状 | 高性能,生产主流 | 性能极差,遗留类,彻底淘汰 |
19. LinkedHashMap 如何实现 LRU 缓存淘汰?
LinkedHashMap 继承 HashMap,额外维护一条双向链表,可实现插入顺序或访问顺序排序,原生支持LRU(最近最少使用)缓存策略。
核心实现方式:开启accessOrder=true(访问顺序模式),重写淘汰策略方法。
// 自定义LRU缓存(固定最大容量,淘汰最久未使用元素)
LinkedHashMap<String, Object> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
// 重写淘汰规则:超过最大容量则移除最久未使用元素
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > 100; // 最大缓存100个元素
}
};
原理:accessOrder=true时,每次get/put访问元素,都会将当前元素移至链表尾部,链表头部始终是最久未使用的元素,超出容量自动淘汰头部元素。
20. TreeMap 核心特点?
-
底层基于红黑树实现,天然有序;
-
排序规则:Key自然排序 或 自定义Comparator排序;
-
支持范围查询:subMap、headMap、tailMap,适配区间数据场景;
-
增删查时间复杂度稳定O(logn);
-
不允许Key为null,Value允许null。
五、并发集合高频面试题(进阶必考)
21. ConcurrentHashMap JDK1.7与1.8实现原理差异?
JDK1.7:分段锁 Segment 机制
-
将整个Map划分为16个独立Segment分段,每个Segment持有独立锁;
-
多线程可同时操作不同Segment,提升并发度;
-
底层:Segment + 数组 + 单向链表,无红黑树;
-
锁粒度大,并发上限有限。
JDK1.8+:CAS + synchronized 细粒度锁(主流)
-
彻底取消Segment分段锁,底层结构与HashMap一致:数组+链表+红黑树;
-
锁粒度大幅细化:仅锁定当前操作的数组桶节点,不同桶可完全并发操作,并发度大幅提升;
-
无冲突用CAS:桶为空时,通过CAS无锁自旋插入节点,性能极高;
-
有冲突用synchronized:桶存在元素时,使用synchronized锁定桶首节点,替代笨重的ReentrantLock;
-
树化并发优化:链表转红黑树、红黑树扩容迁移均支持并发安全操作。
核心总结:JDK1.8通过细粒度桶锁+CAS无锁操作,解决了1.7分段锁并发上限低、锁粒度大的问题,高并发场景性能提升显著。
22. ConcurrentHashMap 为什么放弃 ReentrantLock 改用 synchronized?
-
JDK版本锁优化:JDK1.6之后synchronized进行了大量优化(偏向锁、轻量级锁、自旋锁、锁膨胀),性能基本持平甚至超越ReentrantLock;
-
更低的开销:synchronized是JVM原生锁,无需创建锁对象,内存开销更小、使用更简洁;
-
细粒度场景适配:桶节点级别的短时间加锁场景,synchronized轻量级锁优势明显,无多余上下文切换开销;
-
简化代码架构:减少锁工具类依赖,降低并发逻辑复杂度。
23. ConcurrentHashMap 不支持什么操作?有什么坑?
核心短板:不支持全表原子操作,批量方法非原子性。
-
size()非实时精准:并发读写下,size仅为近似值,无法用于强一致性统计场景;
-
批量方法非原子:putAll、containsValue、clear等方法无全局锁,并发执行会出现数据错乱;
-
弱一致性迭代器:迭代过程中可感知部分新增数据,无法保证快照一致性;
-
key/value禁止null:为了并发判空安全,杜绝空指针歧义。
六、Queue/Deque 队列面试题(线程通信/任务调度)
24. ArrayDeque 和 LinkedList 作为队列的区别?
生产规范:队列优先使用 ArrayDeque,不推荐LinkedList。
| 对比项 | ArrayDeque | LinkedList |
|---|---|---|
| 底层结构 | 可变数组+双指针 | 双向链表 |
| 增删性能 | 极致高效,无节点对象创建开销 | 需频繁创建节点对象,GC开销大 |
| 内存占用 | 紧凑无冗余 | 每个节点存双指针,内存占用高 |
| 适用场景 | 普通队列、栈、双端队列业务 | 极少使用,仅兼容老业务 |
25. 阻塞队列和非阻塞队列区别?生产常用队列?
非阻塞队列:ConcurrentLinkedQueue,基于CAS无锁,高并发读写性能极高,适合纯异步无阻塞任务。
阻塞队列:BlockingQueue 系列,自带锁与阻塞机制,队列满/空时自动阻塞线程,天然适配生产者消费者模型。
生产常用队列:
-
ArrayBlockingQueue:有界阻塞队列,固定容量,防止任务堆积OOM;
-
LinkedBlockingQueue:无界/有界阻塞队列,线程池默认队列;
-
SynchronousQueue:无容量队列,一对一传递任务,适合瞬时高并发;
-
PriorityQueue:优先级任务调度队列。
七、Iterator 迭代器与快速失败(高频坑点)
26. 什么是 fail-fast 快速失败?什么是 fail-safe 安全失败?
1)fail-fast(快速失败)
普通集合(ArrayList/HashMap)迭代时,若检测到集合结构被修改(新增/删除),直接抛出 ConcurrentModificationException,防止脏数据。
触发原理:迭代时校验 modCount(结构修改次数),不一致则抛异常。
2)fail-safe(安全失败)
并发集合(CopyOnWriteArrayList)迭代时,基于原数组快照遍历,迭代期间集合修改不影响当前遍历,不会抛异常,存在数据弱一致性。
27. 为什么迭代时不能直接增删元素?如何正确删除?
普通for/增强for遍历中增删,会修改modCount,触发并发修改异常。
正确写法(生产标准):
// 迭代器安全删除
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("test".equals(s)) {
iterator.remove(); // 迭代器自带删除,同步更新modCount,无异常
}
}
八、Stream流集合处理(生产高频必备+场景题)
Java8 Stream 是生产中集合处理的核心手段,替代繁琐的for循环,代码简洁、高效,适配筛选、分组、排序、去重、聚合、转换等所有集合业务场景,以下为核心考点+真实生产案例。
28. Stream 核心生命周期?
-
创建流:集合/数组转流(stream()、parallelStream());
-
中间操作(延迟执行):filter、map、sorted、distinct、limit等,返回新流,可链式调用;
-
终端操作(触发执行):collect、forEach、count、max、min、anyMatch等,触发计算,关闭流。
核心特点:中间操作懒加载,无终端操作不执行,避免无效计算,提升性能。
29. Stream 常用中间操作&终端操作汇总(生产常用)
| 操作类型 | 常用方法 | 生产用途 |
|---|---|---|
| 中间操作 | filter() | 条件筛选集合元素 |
| 中间操作 | map() | 元素类型转换、字段提取 |
| 中间操作 | sorted() | 集合排序(单字段/多字段升降序) |
| 中间操作 | distinct() | 元素去重(重写equals/hashCode生效) |
| 中间操作 | limit()/skip() | 分页截取集合数据 |
| 终端操作 | collect() | 流转集合、分组、聚合(最核心) |
| 终端操作 | anyMatch/allMatch/noneMatch | 条件匹配判断,返回布尔值 |
| 终端操作 | max/min/count | 集合最值、数量统计 |
30. 生产场景题1:集合筛选、字段提取、过滤空值(高频CRUD)
场景描述:查询用户列表,过滤状态正常、非空用户,提取用户ID集合,用于批量查询关联数据。
// 生产标准写法:过滤+去空+字段提取+转List
List<User> userList = userMapper.selectAllUser();
List<Long> validUserIdList = userList.stream()
.filter(Objects::nonNull) // 过滤null用户对象
.filter(user -> user.getStatus() == 1) // 筛选正常状态用户
.map(User::getId) // 提取用户ID
.collect(Collectors.toList());
31. 生产场景题2:集合分组(订单按用户ID分组,核心业务)
场景描述:查询所有订单数据,根据用户ID分组,得到「用户ID-订单列表」映射,用于批量组装用户订单数据。
// 按用户ID分组,Key=用户ID,Value=对应用户订单集合
List<Order> orderList = orderMapper.selectAllOrder();
Map<Long, List<Order>> userOrderMap = orderList.stream()
.collect(Collectors.groupingBy(Order::getUserId));
进阶分组:分组后聚合统计(每个用户订单数量、订单总金额)
Map<Long, Integer> userOrderCountMap = orderList.stream()
.collect(Collectors.groupingBy(
Order::getUserId,
Collectors.summingInt(Order::getOrderAmount)
));
32. 生产场景题3:集合多字段排序(后台列表分页排序)
场景描述:商品列表排序,优先按价格降序,价格相同按上架时间降序。
List<Goods> goodsList = goodsMapper.selectGoodsList();
List<Goods> sortedGoods = goodsList.stream()
.sorted(Comparator.comparing(Goods::getPrice, Comparator.reverseOrder())
.thenComparing(Goods::getCreateTime, Comparator.reverseOrder()))
.collect(Collectors.toList());
33. 生产场景题4:集合去重(对象字段去重,解决重复数据)
场景描述:用户列表根据手机号去重,保留最新一条数据。
// 基于手机号字段去重
List<User> distinctUserList = userList.stream()
.collect(Collectors.toMap(
User::getPhone,
Function.identity(),
(oldVal, newVal) -> newVal // 重复时保留新数据
))
.values()
.stream()
.collect(Collectors.toList());
34. 生产场景题5:Stream并行流使用规范(大坑避坑)
场景:大批量集合数据处理,想要提升处理速度,使用 parallelStream() 并行流。
生产禁忌(高频事故点):
-
并行流内禁止操作非线程安全变量(ArrayList、普通变量累加会数据错乱);
-
并行流不适合IO密集型任务,仅适合纯内存计算;
-
线程池为全局公共线程池,自定义业务线程池不隔离,易互相影响。
正确使用示例:纯数据转换、统计计算
// 安全:纯内存统计,无共享变量修改
long validCount = userList.parallelStream()
.filter(user -> user.getAge() > 18)
.count();
九、集合生产选型&高频坑点总结(面试绝杀)
35. 业务集合万能选型公式(生产直接套用)
-
普通查询多、增删少:首选 ArrayList
-
频繁中间增删:使用 LinkedList(极少用)
-
单纯去重无序:HashSet
-
去重+保留插入顺序:LinkedHashSet
-
去重+排序:TreeSet
-
单线程键值存储:HashMap
-
需要有序键值:LinkedHashMap/TreeMap
-
并发读多写少:CopyOnWriteArrayList
-
并发键值存储:ConcurrentHashMap
-
队列任务调度:ArrayDeque、阻塞队列
36. 生产高频踩坑清单(必记)
-
ArrayList 无初始容量:大数据量循环add,频繁扩容导致性能暴跌,预估容量优先初始化;
-
重写equals不重写hashCode:HashSet/HashMap去重失效,重复数据堆积;
-
并发用普通集合:多线程操作ArrayList/HashMap,导致数据丢失、死循环、CPU飙高;
-
foreach循环增删元素:必报并发修改异常,必须用迭代器/Stream过滤;
-
滥用并行流:IO任务、共享变量修改导致数据错乱、业务bug;
-
HashMap大量null值/哈希冲突:导致链表过长,查询性能退化严重。
十、综合场景面试真题(大厂压轴)
37. 如何解决 HashMap 大量哈希冲突导致的性能问题?
-
优化hashCode算法:自定义实体类哈希算法,保证哈希值均匀分布;
-
初始化预估容量:根据数据量初始化HashMap容量,减少扩容与冲突;
-
避免自定义对象作为Key:若使用必须重写hashCode和equals,保证稳定性;
-
业务层预处理:提前去重、规整数据,减少哈希碰撞概率;
-
高并发场景替换:并发场景改用ConcurrentHashMap,规避线程问题。
38. 读多写少、写多读少分别用什么集合?生产方案?
-
读多写少:CopyOnWriteArrayList、ConcurrentHashMap,利用读写分离、无锁读提升并发性能;
-
写多读少:规避CopyOnWrite(写开销大),使用ConcurrentLinkedQueue、普通ArrayList(单线程写)、ConcurrentHashMap;
-
极限高并发:结合本地缓存+队列削峰,避免集合频繁读写竞争。
39. Stream处理大数据量集合会OOM吗?如何优化?
会OOM:Stream collect()会一次性加载所有数据到内存,大数据量集合直接流转List会内存溢出。
生产优化方案:
-
大数据量采用分页流式处理,不一次性加载全量数据;
-
使用迭代器分段遍历处理,替代全量Stream转换;
-
并行流处理超大集合时,限制并发数,控制内存占用。
40. 为什么生产中禁止使用 Vector、Hashtable?
两者均为古老遗留类,全局synchronized粗粒度锁,并发阻塞严重、性能极差,且扩容机制不合理、API冗余老旧。生产中完全可以被 ConcurrentHashMap、CopyOnWriteArrayList 替代,无任何使用场景。
更多推荐

所有评论(0)