Java 容器高频面试题(含 JUC)
1. 讲讲常用的 List、Set、Map 有哪些?各自特点?
口述版:
List(有序、可重复):
- ArrayList:基于数组,查询快(O(1)),增删慢(需要挪动元素),线程不安全。
- LinkedList:基于双向链表,增删快,查询慢(要遍历),适合频繁插入删除。
- Vector:和 ArrayList 类似,但是线程安全的(方法加了 synchronized),现在基本不用。
Set(无序、不可重复):
- HashSet:基于 HashMap,无序,查询和增删都接近 O(1)。
- LinkedHashSet:在 HashSet 基础上用链表维护插入顺序。
- TreeSet:基于红黑树(TreeMap),有序(自然排序或自定义比较器)。
Map(键值对):
- HashMap:最常用,线程不安全,允许 null 键和值。
- LinkedHashMap:保持插入顺序或访问顺序。
- TreeMap:基于红黑树,按键排序。
- Hashtable:线程安全,不允许 null,已过时。
- ConcurrentHashMap:线程安全的高性能 Map,JUC 包下。
2. ArrayList 和 LinkedList 的区别?底层实现?
口述版:
| 对比点 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 头部插入/删除 | 慢(要搬移元素) | 快 |
| 尾部插入 | 均摊 O(1) | O(1) |
| 内存占用 | 连续内存,相对紧凑 | 每个节点额外存前后指针 |
| 适用场景 | 读多写少 | 频繁插入删除 |
ArrayList 扩容:
- 默认容量 10。
- 扩容为原来的 1.5 倍(
oldCapacity + (oldCapacity >> 1))。 - 扩容需要复制数组,有一定开销,所以如果能预估大小,最好指定初始容量。
3. HashMap 的底层原理?(重点)
口述版:
HashMap 是面试必考题。
JDK 1.8 之前:
- 数组 + 链表。
- hash 冲突用链表解决(头插法)。
- 多线程扩容时可能形成环形链表,导致死循环。
JDK 1.8 及以后:
- 数组 + 链表 + 红黑树。
- 链表长度超过 8,且数组长度 ≥ 64 时,转成红黑树。
- 链表长度低于 6 时,红黑树退化回链表。
- 使用尾插法,避免多线程扩容死循环(但 HashMap 仍然线程不安全)。
重要参数:
- 默认初始容量:16
- 默认负载因子:0.75
- 扩容阈值:容量 × 负载因子
put 流程简述:
- 计算 key 的 hash
- 定位到数组下标
- 如果位置为空,直接放
- 如果 key 相同,覆盖
- 如果是链表,遍历插入或覆盖;链表过长则树化
- 超过阈值则扩容(2 倍)
4. HashMap 为什么线程不安全?ConcurrentHashMap 怎么解决的?
口述版:
HashMap 线程不安全主要体现在:
- 多线程同时 put 可能导致数据覆盖。
- JDK 1.7 扩容时用头插法,可能形成环形链表,get 时死循环。
- 扩容和读写并发时可能丢失数据。
ConcurrentHashMap 解决方案:
- JDK 1.7:分段锁(Segment),把整个 Map 分成 16 个段,每个段一把锁,不同段可以并发写。
- JDK 1.8:取消分段锁,改用 CAS + synchronized。
- 数组空位用 CAS 放入
- 链表/红黑树的头节点用 synchronized 锁定
- 锁粒度更细,并发度更高
读操作大部分情况下不加锁,性能很好。
5. HashMap 的 hash 方法为什么要右移 16 位再异或?
口述版:
(h = key.hashCode()) ^ (h >>> 16)
目的是让高 16 位也参与到低 16 位的运算中,减少 hash 冲突。
因为数组长度通常是 2 的幂次,最终用 hash & (n-1) 定位。如果只取低位,高位的变化就被浪费了。通过异或高 16 位,能让高位的特征也影响最终下标,分布更均匀。
6. 讲讲 fail-fast 和 fail-safe
口述版:
fail-fast(快速失败):
- 遍历时如果结构被修改(增删),会立刻抛
ConcurrentModificationException。 - ArrayList、HashMap 的迭代器都是 fail-fast。
- 实现靠
modCount和expectedModCount比较。
fail-safe(安全失败):
- 遍历的是容器的一个副本,修改原容器不会影响遍历,也不会抛异常。
- 典型代表:
ConcurrentHashMap、CopyOnWriteArrayList的迭代器。 - 缺点是看到的不一定是最新数据(弱一致性)。
7. CopyOnWriteArrayList 是什么?适用场景和缺点?
口述版:
CopyOnWriteArrayList 是 JUC 下的线程安全 List。
原理: 写操作时,先复制一份新数组,在新数组上修改,然后把引用指向新数组(写时复制)。读操作完全无锁,直接读当前数组。
优点:
- 读多写少场景性能极好,读完全不用加锁。
缺点:
- 写的时候要复制整个数组,内存占用和开销大。
- 数据一致性是最终一致,读可能读到旧数据。
- 不适合写多的场景。
适用场景: 读远多于写的场景,比如黑白名单、监听器列表等。
8. BlockingQueue 有哪些实现?区别是什么?
口述版:
BlockingQueue 是阻塞队列,常用于生产者-消费者模型。
| 实现类 | 特点 |
|---|---|
| ArrayBlockingQueue | 数组实现,有界,需要指定容量 |
| LinkedBlockingQueue | 链表实现,可选有界(默认 Integer.MAX_VALUE) |
| PriorityBlockingQueue | 优先级队列,无界,元素需实现 Comparable |
| DelayQueue | 延时队列,元素需实现 Delayed,到时间才能取出 |
| SynchronousQueue | 不存储元素,每次 put 必须等待 take,直接交接 |
| LinkedTransferQueue | 更强大的 TransferQueue,支持 transfer 直接传递 |
线程池里常用的是 LinkedBlockingQueue 和 SynchronousQueue。
9. ArrayList、Vector、CopyOnWriteArrayList、Collections.synchronizedList 的区别?
口述版:
| 类 | 线程安全 | 实现方式 | 性能特点 |
|---|---|---|---|
| ArrayList | 否 | 数组 | 性能最好,非线程安全 |
| Vector | 是 | 数组 + synchronized | 方法级锁,性能差 |
| Collections.synchronizedList | 是 | 包装 + synchronized | 和 Vector 类似 |
| CopyOnWriteArrayList | 是 | 写时复制 | 读性能极好,写昂贵 |
现在基本不用 Vector,线程安全场景优先考虑 Concurrent 包下的容器,或者自己加锁控制粒度。
10. HashSet 是怎么保证元素不重复的?
口述版:
HashSet 底层其实是 HashMap,元素存在 HashMap 的 key 里,value 用一个固定的 PRESENT 对象占位。
判断是否重复靠两个方法:
- 先比
hashCode() - 如果 hash 相同,再比
equals()
所以自定义对象放进 HashSet/HashMap 时,一定要同时重写 hashCode 和 equals,并且保证相等的对象 hashCode 也要相等。
11. TreeMap / TreeSet 是怎么排序的?
口述版:
底层是红黑树,天然有序。
排序规则:
- 元素实现了
Comparable接口(自然排序) - 或者创建时传入
Comparator比较器(自定义排序)
注意:TreeMap 的 key 或 TreeSet 的元素不能是 null(要比较大小),而且比较必须保持一致性,否则可能出问题。
12. 什么是红黑树?为什么 HashMap 要用红黑树?
口述版:
红黑树是一种自平衡的二叉搜索树,通过颜色和旋转规则保证平衡,查询、插入、删除的时间复杂度都是 O(log n)。
HashMap 在链表过长时转红黑树,是为了防止 hash 冲突严重时退化成链表,查询变成 O(n)。转成红黑树后,最坏情况也能保持 O(log n)。
转化条件:
- 链表长度 ≥ 8 且数组长度 ≥ 64 → 树化
- 红黑树节点 ≤ 6 → 退化回链表
13. ConcurrentHashMap 的 size() 方法是怎么实现的?准确吗?
口述版:
JDK 1.8 中,ConcurrentHashMap 的 size 不是精确加锁统计的。
它用了类似的思想:
- 先尝试无锁统计几次 baseCount + 各个 CounterCell
- 如果竞争激烈,再进入完整的统计(带锁)
所以在高并发下,size() 返回的是一个近似值,不是绝对准确的。这是为了性能做的权衡。如果业务强依赖精确 size,需要自己额外维护计数。
14. 迭代器(Iterator)和 ListIterator 有什么区别?
口述版:
- Iterator:适用于所有 Collection,只能单向遍历(next),可以 remove。
- ListIterator:只适用于 List,支持双向遍历(next/previous),可以 add、set、remove。
普通 for 循环删除 ArrayList 元素容易出问题,推荐用 Iterator 的 remove 方法,或者用 JDK 8 的 removeIf。
15. 如何选择合适的集合类?(总结题)
口述版:
可以按这个思路选:
-
需要键值对 → Map
- 单线程:HashMap
- 要排序:TreeMap
- 要保持插入顺序:LinkedHashMap
- 多线程:ConcurrentHashMap
-
只存元素
- 允许重复、要按下标访问 → ArrayList
- 频繁头尾插入删除 → LinkedList
- 不允许重复、无序 → HashSet
- 不允许重复、要排序 → TreeSet
- 读多写少且要线程安全 → CopyOnWriteArrayList
-
生产者消费者 → BlockingQueue 系列
原则:优先用非线程安全的集合 + 自己控制并发,或者用 JUC 下的并发容器,避免使用 Vector 和 Hashtable。
面试加分点
- 能清楚画出 HashMap 1.7 和 1.8 的结构差异。
- 知道 ConcurrentHashMap 在 1.8 为什么弃用分段锁。
- 能说清 fail-fast 的原理(modCount)。
- 实际项目中用过 CopyOnWriteArrayList 或 BlockingQueue,能讲使用场景。
- 知道自定义对象作为 HashMap key 必须重写 hashCode 和 equals。
- 对红黑树有基本概念(不一定要会手写旋转)。
快速记忆框架:
Java 容器面试核心:
List / Set / Map 基础区别 → HashMap 深度原理(1.7 vs 1.8)→ ConcurrentHashMap → CopyOnWrite → BlockingQueue → fail-fast / fail-safe → 如何选型
把 HashMap 和 ConcurrentHashMap 吃透,再掌握几个常用并发容器,基本就能覆盖大部分容器相关面试题。