Java 集合面试封神笔记:18 道高频真题 + 深度解析(大厂必背)
Java 集合是后端面试必考核心模块,无论校招、社招,阿里、腾讯、字节、美团等大厂必问。它不仅考察 API 使用,更深入底层数据结构、线程安全、JDK 版本差异、性能优化与设计思想。
我整理了 18 道 Java 集合最高频面试题,全部标注掌握程度,附带标准答案、原理深挖、扩展知识点、面试加分项,吃透这篇,集合面试直接稳过。
一、Java 集合整体架构(必背开篇题)
题目:谈谈 Java 中的集合
回答
Java 集合用于存储对象数据,分为 Collection(单值集合)与 Map(键值对集合)两大体系。
1)Collection 体系(存储单个元素)
- List:有序、可重复、支持索引
- ArrayList:动态数组,查询快、增删慢、线程不安全
- LinkedList:双向链表,增删快、查询慢、线程不安全
- Set:无序、不可重复
- HashSet:底层 HashMap,无序、去重
- LinkedHashSet:底层 LinkedHashMap,保留插入顺序
- TreeSet:底层 TreeMap,自动排序(自然排序 / 比较器)
2)Map 体系(存储键值对)
- HashMap:数组 + 链表 + 红黑树(JDK8),无序、键唯一、线程不安全
- LinkedHashMap:继承 HashMap,双向链表维护顺序
- TreeMap:红黑树结构,按键排序
- Hashtable:数组 + 链表,全方法 synchronized,线程安全但低效
扩展知识点
- List 适合有序读取;Set 适合去重;Map 适合键值映射。
- 所有集合只能存储对象,基本类型会自动装箱。
- 集合框架统一了遍历方式:
foreach、Iterator。
二、ArrayList vs LinkedList(必考题)
题目:ArrayList 和 LinkedList 的区别?哪个线程安全?
回答
- 底层结构
- ArrayList:动态数组
- LinkedList:双向链表
- 访问性能
- ArrayList:支持随机访问,查询快 O (1)
- LinkedList:必须遍历,查询慢 O (n)
- 插入删除
- ArrayList:非尾部插入删除需要移动元素,效率低
- LinkedList:任意位置增删快 O (1)
- 内存占用
- ArrayList:连续内存,有扩容冗余,无指针开销
- LinkedList:每个节点存数据 + 前驱 + 后继,内存消耗更大
- 线程安全两者都不安全,并发用
CopyOnWriteArrayList。
适用场景
- 读多写少、随机访问 → ArrayList
- 写多读少、队列 / 栈 / 链表操作 → LinkedList
扩展知识点
LinkedList 实现了 Deque 接口,可以直接当栈、队列、双端队列使用。
三、ArrayList 线程安全问题
题目:ArrayList 线程安全吗?如何变安全?
回答
不安全。多线程同时写会出现数据覆盖、数组越界、数据不一致。
三种安全方案
- CopyOnWriteArrayList(推荐)写时复制,读无锁,写加锁,适合读多写少。
- Collections.synchronizedList()全局锁,简单但并发性能差。
- Vector古老集合,全方法 synchronized,基本不用。
扩展知识点
多线程环境下,遍历 ArrayList 也不安全,因为遍历期间结构可能被修改。
四、ArrayList 扩容机制(高频底层题)
题目:ArrayList 扩容机制
回答
- JDK8 懒加载:new ArrayList () 时数组为
{},第一次 add 才初始化为 10。 - 触发条件:
元素数量+1 > 数组长度。 - 扩容公式:新容量 = 旧容量 + 旧容量 >> 1 = 1.5 倍
- 扩容流程计算新容量 → 创建新数组 → 复制数据 → 引用指向新数组
- 优化提前预知容量时,使用
ensureCapacity(int)预先扩容,避免多次拷贝。
扩展知识点
ArrayList 最大容量受限于 Integer.MAX_VALUE - 8,防止溢出。
五、CopyOnWriteArrayList 实现原理(高频)
题目:CopyOnWriteArrayList 如何实现线程安全?
回答
- 底层数组 + volatile数组用
volatile修饰,保证修改后其他线程立即可见。 - 写加锁(ReentrantLock)新增 / 删除时:
- 加锁
- 复制一个新数组
- 写入新元素
- 把引用指向新数组
- 读完全无锁读操作直接读取当前数组,高性能。
优点
- 读非常快
- 多线程下不会抛出并发修改异常
缺点
- 写会占用双倍内存
- 最终一致性,读取可能是旧数据
适用场景
读多写少:白名单、配置、缓存、商品类目等。
六、HashMap 底层原理(最核心面试题)
题目:HashMap 实现原理?为什么用红黑树?阈值为什么是 8?
回答
1)JDK7 vs JDK8
- JDK7:数组 + 链表,头插法,多线程扩容会形成环形链表。
- JDK8:数组 + 链表 + 红黑树,尾插法,解决死循环问题。
2)put 流程简化
计算 hash → 寻址 → 空桶直接插入 → 冲突则链表 / 树 → 长度≥8 且数组≥64 树化 → 超过阈值扩容
3)为什么引入红黑树?
链表太长时,查询从 O (n) 降为 O(logn),保证极端情况下性能不雪崩。
4)树化阈值为什么是 8?
根据泊松分布,哈希正常时,链表长度达到 8 的概率极小。
- ≤6:链表效率足够
- 8:概率极低,视为哈希攻击 / 异常,转为红黑树保底
- 中间值 7 用于防止频繁转换
扩展知识点
红黑树退化阈值是 6,避免频繁树化 / 链表化抖动。
七、为什么 HashMap 不用 AVL 树?
题目:HashMap 为什么不用平衡二叉树?
回答
- AVL 树:严格平衡,高度差 ≤1,查询极快,但插入删除旋转非常多。
- 红黑树:近似平衡,旋转次数少,增删效率远高于 AVL。
HashMap 是读写都频繁的结构,红黑树能在查询效率 + 维护成本之间达到最佳平衡。
红黑树 5 大特性(背会直接加分)
- 节点只有红 / 黑
- 根节点黑色
- 没有连续红节点
- 叶子节点(null)黑色
- 任意节点到叶子的所有路径黑节点数量相同
八、HashMap 线程安全问题
题目:HashMap 线程安全吗?
回答
不安全。
- JDK7:多线程扩容会死循环、数据丢失
- JDK8:修复死循环,但仍会数据覆盖
安全方案
- ConcurrentHashMap(高并发首选)
- Collections.synchronizedMap(全局锁,性能差)
扩展知识点
HashMap 不允许在迭代时修改结构,否则抛出 ConcurrentModificationException。
九、HashMap get /put 流程(必背)
题目:HashMap get 过程
- 计算 key 的 hash
- 定位数组下标
- 检查头节点:key 相等直接返回
- 是树 → 树查找
- 是链表 → 遍历查找
- 找不到返回 null
题目:HashMap put 过程
- 数组为空则懒初始化
- 计算 hash 寻址
- 空桶直接插入
- 头节点 key 相同则覆盖
- 树 → 树插入
- 链表 → 尾插,判断是否树化
- size 超过阈值则2 倍扩容
- 返回旧值
十、HashMap get 方法一定安全吗?
题目:HashMap 调用 get 一定安全吗?
回答
不安全!
- 单线程未初始化时 get 会出错;初始化后没问题。
- 多线程
- 读到旧值 /null
- JDK7 可能触发死循环
- JDK8 结构修改可能导致遍历异常
结论:多线程下绝对不能用 HashMap,必须用 ConcurrentHashMap。
十一、重写 hashCode 和 equals(高频)
题目:重写 HashMap 的 hashCode 和 equals 要注意什么?
回答
核心规则:
- equals 为 true → hashCode 必须相同
- hashCode 相同 → equals 不一定相同(哈希冲突)
错误后果
- hashCode 不一致 → 存得进、取不到
- equals 错误 → 键重复、数据覆盖
- 只重写一个 → 集合完全异常
最佳实践
必须同时重写,使用对象中相同的字段参与计算。
十二、HashMap 扩容机制
题目:HashMap 扩容机制
回答
- 负载因子默认 0.75阈值 = 容量 × 0.75
- 触发条件size > 阈值树化时如果数组长度 <64 → 优先扩容,不树化
- 扩容规则新容量 = 旧容量 × 2
- 迁移规则元素下标:要么不变,要么 原下标 + 旧容量
为什么快?
不用重新 hash,只用位运算判断最高位。
十三、HashMap 容量为什么必须是 2 的 n 次方?
题目:HashMap 大小为什么是 2 的 n 次方?
回答(3 个核心原因)
- 取模变位运算,更快
hash & (length-1)等价取模,但速度高几个数量级 - 分布均匀,减少冲突length-1 低位全 1,hash 低位全部参与计算
- 扩容迁移极快只需判断一位,就能确定新位置
扩展知识点
如果你传入非 2 的幂,HashMap 会自动向上取为最近 2 的幂。
十四、HashMap 负载因子 0.75
题目:说说 HashMap 的负载因子
回答
默认 0.75,是时间与空间的平衡:
- 太小 → 浪费内存,频繁扩容
- 太大 → 冲突暴增,链表变长,查询变慢
0.75 能让冲突概率最低,性能最平稳。
十五、ConcurrentHashMap(大厂必问)
题目:ConcurrentHashMap 怎么实现?
回答
JDK7
- Segment + HashEntry
- 分段锁:一把锁锁一段数据
- 并发度 = 段数(默认 16)
JDK8(重点)
- 数组 + 链表 + 红黑树
- CAS + synchronized
- 锁粒度:只锁一个桶的头节点
put 流程
- 空数组 → CAS 初始化
- 目标桶空 → CAS 插入
- 桶非空 → synchronized 锁住头节点 插入
- 判断树化
- 扩容协助
扩展知识点
JDK8 支持多线程协助扩容,速度极快。
十六、JDK8 为什么弃用 Segment?
题目:为什么放弃分段锁?
回答
- 内存占用大Segment 继承 ReentrantLock,结构重,占内存
- 并发度上限固定(默认 16)
- JDK8 对 synchronized 极致优化偏向锁→轻量级锁→自旋锁→重量级锁,性能不输 Lock
- 扩容效率低分段只能段内扩容,无法全局优化
十七、为什么 ConcurrentHashMap 用 CAS + synchronized?
题目:已经用 synchronized 了,为什么还要 CAS?
回答
分层锁策略,性能最大化:
- CAS 无锁、无阻塞、无开销适合并发低、桶为空的场景,一次成功。
- synchronized 稳定安全适合冲突高的场景,避免 CAS 无限自旋浪费 CPU。
先无锁,后加锁,是高并发设计的经典范式。
十八、HashMap vs Hashtable vs ConcurrentHashMap
题目:说一下三者区别
回答
1)HashMap
- 线程不安全
- 键值可以为 null(键只能一个 null)
- 初始容量 16
- 扩容 2 倍
- JDK8:数组 + 链表 + 红黑树
2)Hashtable
- 线程安全(全方法 synchronized)
- 键值不能为 null
- 初始容量 11
- 扩容 2n+1
- 性能极差,几乎废弃
3)ConcurrentHashMap
- 线程安全
- 键值不能为 null
- JDK8:CAS + synchronized + 桶锁
- 高并发性能优秀
扩展知识点
ConcurrentHashMap 不支持 null 是为了避免二义性:无法判断是不存在还是值为 null。
更多推荐

所有评论(0)