前言

Java 集合是 Java 基础中非常重要的一部分,也是面试中的高频考点。

很多同学平时写代码经常用:

ArrayList HashMap HashSet LinkedList ConcurrentHashMap

但是面试的时候,面试官往往不会只问“会不会用”,而是会继续追问:

  • ArrayList 和 LinkedList 有什么区别?
  • HashMap 底层原理是什么?
  • HashMap 为什么线程不安全?
  • HashMap 为什么容量是 2 的幂?
  • ConcurrentHashMap 是怎么保证线程安全的?
  • fail-fast 是什么?

这篇文章整理一份 Java 集合高频八股文,适合初学者和准备面试的同学快速复习。


1. Java 集合框架主要分为哪几类?

Java 集合框架主要分为两大类:

Collection
Map

Collection

Collection 主要存储单个元素。

它下面常见的子接口有:

List Set Queue

Map

Map 主要存储键值对,也就是:

key-value

常见集合类有:

ArrayList LinkedList HashSet TreeSet HashMap TreeMap ConcurrentHashMap

简单理解:

Collection 存的是一个个元素,Map 存的是一组组键值对。


2. List、Set、Map 有什么区别?

类型 特点 常见实现
List 有序、可重复 ArrayList、LinkedList
Set 无序、不可重复 HashSet、TreeSet
Map 键值对,key 不可重复 HashMap、TreeMap

List

List 中的元素有下标,可以通过索引访问。

list.get(0);

Set

Set 中的元素不能重复。

常用于去重。

Map

Map 以 key-value 形式存储数据。

map.put("name", "zhangsan");

3. ArrayList 底层原理是什么?

ArrayList 底层是基于数组实现的。

数组的特点是:

  1. 查询快。
  2. 增删慢。
  3. 内存连续。
  4. 支持下标访问。

例如:

List<String> list = new ArrayList<>(); list.add("Java"); list.add("MySQL");

当调用 add() 方法时,ArrayList 会把元素放入底层数组中。

如果数组容量不够,就会触发扩容。


4. ArrayList 默认容量是多少?

在 JDK 8 中,创建空的 ArrayList:

List<String> list = new ArrayList<>();

此时底层数组还是一个空数组。

当第一次添加元素时,才会初始化容量为:

10

也就是说:

ArrayList 是懒加载式初始化数组,第一次 add 时默认容量才变成 10。


5. ArrayList 如何扩容?

ArrayList 容量不够时会自动扩容。

在 JDK 8 中,扩容后的容量大约是原来的:

1.5 倍

例如原容量是 10,扩容后大约变成 15。

扩容过程大致是:

  1. 创建一个更大的新数组。
  2. 把旧数组中的元素复制到新数组。
  3. 把新数组作为底层数组。

所以如果提前知道数据量,建议使用带容量的构造方法:

List<String> list = new ArrayList<>(1000);

这样可以减少扩容次数,提高性能。


6. ArrayList 和 LinkedList 有什么区别?

对比项 ArrayList LinkedList
底层结构 数组 双向链表
查询
随机访问 支持 不适合
头尾增删 较慢 较快
内存占用 较少 较多

ArrayList 适合查询多、增删少的场景。

LinkedList 适合头尾增删较多的场景。

实际开发中,大多数情况下使用 ArrayList 更多。


7. LinkedList 底层原理是什么?

LinkedList 底层是双向链表。

每个节点中包含:

  1. 当前元素。
  2. 前一个节点引用。
  3. 后一个节点引用。

结构类似:

prev <- node -> next

因为每个节点都保存前后引用,所以 LinkedList 插入和删除节点时,不需要移动大量元素。

但是查询某个位置的元素时,需要从头或尾开始遍历,所以随机访问效率较低。


8. HashMap 底层原理是什么?

HashMap 是面试中的重点。

在 JDK 8 中,HashMap 底层结构是:

数组 + 链表 + 红黑树

当我们执行:

map.put("name", "zhangsan");

大致流程是:

  1. 根据 key 计算 hash 值。
  2. 根据 hash 值计算数组下标。
  3. 如果该位置没有元素,直接插入。
  4. 如果该位置有元素,说明发生哈希冲突。
  5. 冲突时先用链表存储。
  6. 链表过长时会转换为红黑树。

简单理解:

HashMap 通过 hash 定位数组位置,如果冲突就用链表或红黑树解决。


9. 什么是哈希冲突?

哈希冲突指的是:

不同的 key 经过 hash 计算后,落到了数组的同一个位置。

例如:

key1 -> 下标 3 key2 -> 下标 3

这就发生了哈希冲突。

HashMap 解决哈希冲突的方式是:

链表 + 红黑树

JDK 8 中,当链表长度达到一定条件时,会转换为红黑树,提高查询效率。


10. HashMap 什么时候会转成红黑树?

在 JDK 8 中,HashMap 链表转红黑树需要满足两个主要条件:

链表长度 >= 8 数组容量 >= 64

如果链表长度达到 8,但数组容量小于 64,HashMap 会优先选择扩容,而不是树化。

这样做的原因是:

数据量较小时,扩容通常比树化更合适。


11. HashMap 的默认初始容量是多少?

HashMap 默认初始容量是:

16

默认负载因子是:

0.75

当元素数量超过:

容量 * 负载因子

时,就会触发扩容。

例如默认容量 16,负载因子 0.75,那么阈值是:

16 * 0.75 = 12

当元素数量超过 12 时,就会扩容。


12. HashMap 为什么容量是 2 的幂?

HashMap 的容量设计为 2 的幂,主要是为了提高取模效率和减少哈希冲突。

一般计算数组下标可以用:

hash % length

但是取模运算性能相对较低。

当 length 是 2 的幂时,可以使用位运算:

hash & (length - 1)

位运算效率更高。

同时,容量是 2 的幂可以让数据分布更均匀,减少哈希冲突。


13. HashMap 的 put 流程是什么?

HashMap 的 put 流程大致如下:

  1. 判断数组是否为空,如果为空则初始化。
  2. 根据 key 计算 hash 值。
  3. 根据 hash 值计算数组下标。
  4. 如果该位置为空,直接插入新节点。
  5. 如果该位置不为空,判断 key 是否相同。
  6. key 相同则覆盖旧值。
  7. key 不同则插入链表或红黑树。
  8. 插入后判断是否需要扩容。

面试中可以这样总结:

HashMap 先根据 key 的 hash 值定位桶位置,如果桶为空就直接插入;如果桶不为空,就判断 key 是否已存在,存在则覆盖,不存在则挂到链表或红黑树中,最后根据元素数量判断是否扩容。


14. HashMap 的 get 流程是什么?

HashMap 的 get 流程大致如下:

  1. 根据 key 计算 hash 值。
  2. 根据 hash 值计算数组下标。
  3. 找到对应桶位置。
  4. 判断第一个节点的 key 是否匹配。
  5. 如果匹配,直接返回 value。
  6. 如果不匹配,继续在链表或红黑树中查找。
  7. 找到就返回 value,找不到就返回 null。

核心就是:

hash 定位数组下标,然后在桶内查找 key


15. HashMap 为什么线程不安全?

HashMap 是线程不安全的。

原因包括:

  1. 多线程同时 put,可能导致数据覆盖。
  2. 多线程扩容时,可能造成数据丢失。
  3. 读写并发时,可能读到不一致的数据。
  4. JDK 7 中多线程扩容甚至可能形成死循环链表。

如果在多线程环境下使用 Map,可以选择:

ConcurrentHashMap

或者:

Collections.synchronizedMap(new HashMap<>());

实际开发中更推荐 ConcurrentHashMap。


16. HashMap 和 Hashtable 有什么区别?

对比项 HashMap Hashtable
线程安全 不安全 安全
性能 较高 较低
null key/value 允许 不允许
出现时间 较新 较老
推荐程度 常用 不推荐

Hashtable 的方法基本都使用 synchronized 修饰,所以线程安全,但性能较差。

现在实际开发中很少使用 Hashtable。


17. HashMap 和 ConcurrentHashMap 有什么区别?

对比项 HashMap ConcurrentHashMap
线程安全 不安全 安全
使用场景 单线程 多线程
是否允许 null 允许 null key 和 null value 不允许 null key 和 null value
性能 单线程下较好 并发场景下较好

如果是普通单线程业务,使用 HashMap。

如果是多线程共享数据,使用 ConcurrentHashMap。


18. ConcurrentHashMap 底层原理是什么?

JDK 8 中,ConcurrentHashMap 底层结构也是:

数组 + 链表 + 红黑树

它主要通过:

CAS + synchronized

来保证线程安全。

大致逻辑:

  1. 插入空桶时,使用 CAS。
  2. 桶中已有元素时,使用 synchronized 锁住桶头节点。
  3. 扩容时多个线程可以协助扩容。
  4. 读取操作通常不加锁,提高并发性能。

相比 Hashtable 全表加锁,ConcurrentHashMap 锁粒度更小,性能更好。


19. ConcurrentHashMap 为什么不允许 null?

ConcurrentHashMap 不允许 null key 和 null value。

主要原因是:

在并发环境下,null 会造成歧义。

例如:

map.get("name")

如果返回 null,无法判断:

  1. key 不存在。
  2. key 存在但 value 是 null。

在并发情况下,这种判断更容易出问题。

所以 ConcurrentHashMap 直接禁止 null。


20. HashSet 底层原理是什么?

HashSet 底层其实是 HashMap。

HashSet 添加元素时,本质上是把元素作为 HashMap 的 key。

value 使用一个固定的 Object 对象占位。

例如:

set.add("Java");

底层类似:

map.put("Java", PRESENT);

所以 HashSet 能去重,本质上依赖的是 HashMap 的 key 不重复。


21. TreeMap 和 TreeSet 有什么特点?

TreeMap 和 TreeSet 底层都是红黑树。

特点是:

  1. 元素有序。
  2. 查询、插入、删除时间复杂度是 O(log n)。
  3. 可以按照自然顺序排序。
  4. 也可以自定义比较器排序。

例如:

TreeSet<Integer> set = new TreeSet<>(); set.add(3); set.add(1); set.add(2);

输出时会按照顺序:

1, 2, 3

如果需要排序集合,可以考虑 TreeMap 或 TreeSet。


22. 什么是 fail-fast?

fail-fast 是 Java 集合中的快速失败机制。

当一个线程正在遍历集合时,如果另一个线程修改了集合结构,就可能抛出:

ConcurrentModificationException

例如:

List<String> list = new ArrayList<>();
list.add("Java");
list.add("MySQL");

for (String item : list) {
    list.remove(item);
}

这段代码可能会触发 fail-fast。

原因是集合在遍历过程中被修改了。


23. 如何避免 ConcurrentModificationException?

可以使用迭代器的 remove() 方法:

Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
    String item = iterator.next();
    if ("Java".equals(item)) {
        iterator.remove();
    }
}

或者使用:

removeIf

例如:

list.removeIf(item -> "Java".equals(item));

如果是并发场景,可以使用并发集合,例如:

CopyOnWriteArrayList ConcurrentHashMap


24. 什么是 fail-safe?

fail-safe 是安全失败机制。

它通常不会直接在原集合上遍历,而是遍历集合的副本。

例如:

CopyOnWriteArrayList

遍历时即使其他线程修改集合,也不会抛出 ConcurrentModificationException。

但是它有一个缺点:

遍历时看到的数据可能不是最新的。

所以 fail-safe 更适合读多写少的场景。


25. Iterator 和 foreach 有什么关系?

foreach 本质上是语法糖。

例如:

for (String item : list) {
    System.out.println(item);
}

底层其实是使用 Iterator 进行遍历。

所以在 foreach 遍历过程中,直接调用集合的 remove 方法,可能会触发 fail-fast。

如果需要边遍历边删除,建议使用 Iterator 的 remove 方法。


26. CopyOnWriteArrayList 是什么?

CopyOnWriteArrayList 是线程安全的 List。

它的核心思想是:

写时复制

读操作不加锁。

写操作时,会复制一份新数组,在新数组上修改,修改完成后再替换旧数组。

优点:

  1. 读操作性能高。
  2. 遍历时不会抛出 ConcurrentModificationException。
  3. 适合读多写少场景。

缺点:

  1. 写操作开销大。
  2. 占用更多内存。
  3. 数据存在短暂不一致。

常见场景:

配置列表 黑名单列表 监听器列表


27. ArrayList 和 Vector 有什么区别?

对比项 ArrayList Vector
线程安全 不安全 安全
性能 较高 较低
扩容 约 1.5 倍 默认 2 倍
推荐程度 常用 较少使用

Vector 的方法大多使用 synchronized 修饰,所以线程安全,但性能较差。

现在实际开发中很少使用 Vector。


28. Queue 和 Deque 有什么区别?

Queue

Queue 是队列,通常是先进先出。

FIFO

常见方法:

offer()
poll()
peek()

Deque

Deque 是双端队列,两端都可以插入和删除。

既可以当队列使用,也可以当栈使用。

常见实现:

ArrayDeque LinkedList

实际开发中,如果需要栈结构,推荐使用:

ArrayDeque

而不是老的 Stack。


29. Java 集合如何选择?

可以按场景选择:

查询多、增删少

ArrayList

需要去重

HashSet

需要排序

TreeSet TreeMap

存储 key-value

HashMap

多线程 Map

ConcurrentHashMap

读多写少的线程安全 List

CopyOnWriteArrayList

队列

ArrayDeque LinkedList PriorityQueue


30. Java 集合面试怎么回答更稳?

回答集合八股时,不要只背定义,可以按照:

底层结构 + 特点 + 使用场景 + 注意点

比如问:“ArrayList 和 LinkedList 有什么区别?”

可以这样回答:

ArrayList 底层是数组,支持随机访问,所以查询速度快,但中间插入和删除时需要移动元素。LinkedList 底层是双向链表,插入和删除节点比较方便,但查询元素需要遍历链表,所以随机访问慢。实际开发中,如果没有特殊的频繁头尾增删需求,一般优先使用 ArrayList。

再比如问:“HashMap 底层原理是什么?”

可以这样回答:

JDK 8 中 HashMap 底层是数组、链表和红黑树。put 数据时,会先根据 key 计算 hash,再根据 hash 计算数组下标。如果桶为空就直接插入,如果发生哈希冲突,就用链表存储;当链表长度达到 8 并且数组容量大于等于 64 时,会转换成红黑树,提高查询效率。HashMap 默认容量是 16,负载因子是 0.75,超过阈值会进行扩容。


总结

Java 集合是 Java 基础面试中非常重要的一块。

高频内容主要集中在:

集合体系 ArrayList LinkedList HashMap HashSet ConcurrentHashMap TreeMap fail-fast CopyOnWriteArrayList 集合选择

其中最重要的一定是:

  1. ArrayList 底层和扩容机制。
  2. ArrayList 和 LinkedList 的区别。
  3. HashMap 底层结构。
  4. HashMap 的 put 流程。
  5. HashMap 为什么线程不安全。
  6. ConcurrentHashMap 如何保证线程安全。
  7. fail-fast 是什么。

如果是初学者,建议先把这些问题理解清楚,再去结合源码阅读。面试时不用一上来就讲得特别深,但一定要能把底层结构、使用场景和注意点说清楚。

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐