Administrator 23 阅读 面试

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 流程简述:

  1. 计算 key 的 hash
  2. 定位到数组下标
  3. 如果位置为空,直接放
  4. 如果 key 相同,覆盖
  5. 如果是链表,遍历插入或覆盖;链表过长则树化
  6. 超过阈值则扩容(2 倍)

4. HashMap 为什么线程不安全?ConcurrentHashMap 怎么解决的?

口述版:

HashMap 线程不安全主要体现在:

  1. 多线程同时 put 可能导致数据覆盖。
  2. JDK 1.7 扩容时用头插法,可能形成环形链表,get 时死循环。
  3. 扩容和读写并发时可能丢失数据。

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。

原理: 写操作时,先复制一份新数组,在新数组上修改,然后把引用指向新数组(写时复制)。读操作完全无锁,直接读当前数组。

优点:

  • 读多写少场景性能极好,读完全不用加锁。

缺点:

  1. 写的时候要复制整个数组,内存占用和开销大。
  2. 数据一致性是最终一致,读可能读到旧数据。
  3. 不适合写多的场景。

适用场景: 读远多于写的场景,比如黑白名单、监听器列表等。


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 对象占位。

判断是否重复靠两个方法:

  1. 先比 hashCode()
  2. 如果 hash 相同,再比 equals()

所以自定义对象放进 HashSet/HashMap 时,一定要同时重写 hashCode 和 equals,并且保证相等的对象 hashCode 也要相等。


11. TreeMap / TreeSet 是怎么排序的?

口述版:

底层是红黑树,天然有序。

排序规则:

  1. 元素实现了 Comparable 接口(自然排序)
  2. 或者创建时传入 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. 如何选择合适的集合类?(总结题)

口述版:

可以按这个思路选:

  1. 需要键值对 → Map

    • 单线程:HashMap
    • 要排序:TreeMap
    • 要保持插入顺序:LinkedHashMap
    • 多线程:ConcurrentHashMap
  2. 只存元素

    • 允许重复、要按下标访问 → ArrayList
    • 频繁头尾插入删除 → LinkedList
    • 不允许重复、无序 → HashSet
    • 不允许重复、要排序 → TreeSet
    • 读多写少且要线程安全 → CopyOnWriteArrayList
  3. 生产者消费者 → BlockingQueue 系列

原则:优先用非线程安全的集合 + 自己控制并发,或者用 JUC 下的并发容器,避免使用 Vector 和 Hashtable。


面试加分点

  1. 能清楚画出 HashMap 1.7 和 1.8 的结构差异。
  2. 知道 ConcurrentHashMap 在 1.8 为什么弃用分段锁。
  3. 能说清 fail-fast 的原理(modCount)。
  4. 实际项目中用过 CopyOnWriteArrayList 或 BlockingQueue,能讲使用场景。
  5. 知道自定义对象作为 HashMap key 必须重写 hashCode 和 equals。
  6. 对红黑树有基本概念(不一定要会手写旋转)。

快速记忆框架:

Java 容器面试核心:

List / Set / Map 基础区别 → HashMap 深度原理(1.7 vs 1.8)→ ConcurrentHashMap → CopyOnWrite → BlockingQueue → fail-fast / fail-safe → 如何选型

把 HashMap 和 ConcurrentHashMap 吃透,再掌握几个常用并发容器,基本就能覆盖大部分容器相关面试题。