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 适合键值映射。
  • 所有集合只能存储对象,基本类型会自动装箱。
  • 集合框架统一了遍历方式:foreachIterator

二、ArrayList vs LinkedList(必考题)

题目:ArrayList 和 LinkedList 的区别?哪个线程安全?

回答

  1. 底层结构
    • ArrayList:动态数组
    • LinkedList:双向链表
  2. 访问性能
    • ArrayList:支持随机访问,查询快 O (1)
    • LinkedList:必须遍历,查询慢 O (n)
  3. 插入删除
    • ArrayList:非尾部插入删除需要移动元素,效率低
    • LinkedList:任意位置增删快 O (1)
  4. 内存占用
    • ArrayList:连续内存,有扩容冗余,无指针开销
    • LinkedList:每个节点存数据 + 前驱 + 后继,内存消耗更大
  5. 线程安全两者都不安全,并发用 CopyOnWriteArrayList

适用场景

  • 读多写少、随机访问 → ArrayList
  • 写多读少、队列 / 栈 / 链表操作 → LinkedList

扩展知识点

LinkedList 实现了 Deque 接口,可以直接当栈、队列、双端队列使用。


三、ArrayList 线程安全问题

题目:ArrayList 线程安全吗?如何变安全?

回答

不安全。多线程同时写会出现数据覆盖、数组越界、数据不一致

三种安全方案

  1. CopyOnWriteArrayList(推荐)写时复制,读无锁,写加锁,适合读多写少
  2. Collections.synchronizedList()全局锁,简单但并发性能差。
  3. Vector古老集合,全方法 synchronized,基本不用。

扩展知识点

多线程环境下,遍历 ArrayList 也不安全,因为遍历期间结构可能被修改。


四、ArrayList 扩容机制(高频底层题)

题目:ArrayList 扩容机制

回答

  1. JDK8 懒加载:new ArrayList () 时数组为 {},第一次 add 才初始化为 10
  2. 触发条件元素数量+1 > 数组长度
  3. 扩容公式:新容量 = 旧容量 + 旧容量 >> 1 = 1.5 倍
  4. 扩容流程计算新容量 → 创建新数组 → 复制数据 → 引用指向新数组
  5. 优化提前预知容量时,使用 ensureCapacity(int) 预先扩容,避免多次拷贝。

扩展知识点

ArrayList 最大容量受限于 Integer.MAX_VALUE - 8,防止溢出。


五、CopyOnWriteArrayList 实现原理(高频)

题目:CopyOnWriteArrayList 如何实现线程安全?

回答

  1. 底层数组 + volatile数组用 volatile 修饰,保证修改后其他线程立即可见。
  2. 写加锁(ReentrantLock)新增 / 删除时:
    • 加锁
    • 复制一个新数组
    • 写入新元素
    • 把引用指向新数组
  3. 读完全无锁读操作直接读取当前数组,高性能。

优点

  • 读非常快
  • 多线程下不会抛出并发修改异常

缺点

  • 写会占用双倍内存
  • 最终一致性,读取可能是旧数据

适用场景

读多写少:白名单、配置、缓存、商品类目等。


六、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 大特性(背会直接加分)

  1. 节点只有红 / 黑
  2. 根节点黑色
  3. 没有连续红节点
  4. 叶子节点(null)黑色
  5. 任意节点到叶子的所有路径黑节点数量相同

八、HashMap 线程安全问题

题目:HashMap 线程安全吗?

回答

不安全

  • JDK7:多线程扩容会死循环、数据丢失
  • JDK8:修复死循环,但仍会数据覆盖

安全方案

  1. ConcurrentHashMap(高并发首选)
  2. Collections.synchronizedMap(全局锁,性能差)

扩展知识点

HashMap 不允许在迭代时修改结构,否则抛出 ConcurrentModificationException


九、HashMap get /put 流程(必背)

题目:HashMap get 过程

  1. 计算 key 的 hash
  2. 定位数组下标
  3. 检查头节点:key 相等直接返回
  4. 是树 → 树查找
  5. 是链表 → 遍历查找
  6. 找不到返回 null

题目:HashMap put 过程

  1. 数组为空则懒初始化
  2. 计算 hash 寻址
  3. 空桶直接插入
  4. 头节点 key 相同则覆盖
  5. 树 → 树插入
  6. 链表 → 尾插,判断是否树化
  7. size 超过阈值则2 倍扩容
  8. 返回旧值

十、HashMap get 方法一定安全吗?

题目:HashMap 调用 get 一定安全吗?

回答

不安全!

  1. 单线程未初始化时 get 会出错;初始化后没问题。
  2. 多线程
    • 读到旧值 /null
    • JDK7 可能触发死循环
    • JDK8 结构修改可能导致遍历异常

结论:多线程下绝对不能用 HashMap,必须用 ConcurrentHashMap。


十一、重写 hashCode 和 equals(高频)

题目:重写 HashMap 的 hashCode 和 equals 要注意什么?

回答

核心规则:

  • equals 为 true → hashCode 必须相同
  • hashCode 相同 → equals 不一定相同(哈希冲突)

错误后果

  • hashCode 不一致 → 存得进、取不到
  • equals 错误 → 键重复、数据覆盖
  • 只重写一个 → 集合完全异常

最佳实践

必须同时重写,使用对象中相同的字段参与计算。


十二、HashMap 扩容机制

题目:HashMap 扩容机制

回答

  1. 负载因子默认 0.75阈值 = 容量 × 0.75
  2. 触发条件size > 阈值树化时如果数组长度 <64 → 优先扩容,不树化
  3. 扩容规则新容量 = 旧容量 × 2
  4. 迁移规则元素下标:要么不变,要么 原下标 + 旧容量

为什么快?

不用重新 hash,只用位运算判断最高位。


十三、HashMap 容量为什么必须是 2 的 n 次方?

题目:HashMap 大小为什么是 2 的 n 次方?

回答(3 个核心原因)

  1. 取模变位运算,更快hash & (length-1) 等价取模,但速度高几个数量级
  2. 分布均匀,减少冲突length-1 低位全 1,hash 低位全部参与计算
  3. 扩容迁移极快只需判断一位,就能确定新位置

扩展知识点

如果你传入非 2 的幂,HashMap 会自动向上取为最近 2 的幂。


十四、HashMap 负载因子 0.75

题目:说说 HashMap 的负载因子

回答

默认 0.75,是时间与空间的平衡:

  • 太小 → 浪费内存,频繁扩容
  • 太大 → 冲突暴增,链表变长,查询变慢

0.75 能让冲突概率最低,性能最平稳。


十五、ConcurrentHashMap(大厂必问)

题目:ConcurrentHashMap 怎么实现?

回答

JDK7

  • Segment + HashEntry
  • 分段锁:一把锁锁一段数据
  • 并发度 = 段数(默认 16)

JDK8(重点)

  • 数组 + 链表 + 红黑树
  • CAS + synchronized
  • 锁粒度:只锁一个桶的头节点

put 流程

  1. 空数组 → CAS 初始化
  2. 目标桶空 → CAS 插入
  3. 桶非空 → synchronized 锁住头节点 插入
  4. 判断树化
  5. 扩容协助

扩展知识点

JDK8 支持多线程协助扩容,速度极快。


十六、JDK8 为什么弃用 Segment?

题目:为什么放弃分段锁?

回答

  1. 内存占用大Segment 继承 ReentrantLock,结构重,占内存
  2. 并发度上限固定(默认 16)
  3. JDK8 对 synchronized 极致优化偏向锁→轻量级锁→自旋锁→重量级锁,性能不输 Lock
  4. 扩容效率低分段只能段内扩容,无法全局优化

十七、为什么 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。

更多推荐