容器高频面试题
定位说明
本篇把 02-Java容器 全块的高频面试问题按族收拢:每题给要点式答案 + 指路详篇。答案只列"考官想听的骨架",完整机制回详篇读。刷前先过 00-容器选型总对比 的总表。
List 族
Q1:ArrayList 和 LinkedList 的区别?什么时候选谁?
- 底层:动态数组(O(1) 随机访问,扩容 1.5 倍拷贝) vs 双向链表(头尾 O(1),随机访问 O(n))
- 选型:默认 ArrayList;头尾高频增删(队列/栈场景)用 ArrayDeque 而不是 LinkedList;LinkedList 的独特价值是"同时要 List 又要 Deque"的少数场景
- 反直觉点:LinkedList 中间插入并不快——定位是 O(n),只有"已持有节点引用"时改指针才是 O(1)
- 详见 ArrayList / LinkedList
Q2:ArrayList 扩容机制?为什么是 1.5 倍?
grow():newCapacity = old + (old >> 1);Arrays.copyOf整体迁移- 无参构造懒惰初始化:首次 add 才分配 10
- 1.5 倍是折中:2 倍浪费内存(Vector 的教训),太小扩容频繁;位运算
>>1快 - 大数据量先
new ArrayList<>(预估容量)免反复拷贝 - 详见 ArrayList 扩容机制节
Q3:什么是 fail-fast?怎么安全地遍历删除?
modCount结构修改计数器;迭代器发现不一致立即抛ConcurrentModificationException- 安全删除:迭代器自己的
it.remove()(同步更新 expectedModCount);或removeIf() - 本质是"尽力而为"的 bug 探测器,不是并发保证——多线程该用并发容器
- 详见 ArrayList Fail-Fast 节
Q4:为什么不用 Vector?CopyOnWriteArrayList 好在哪?
- Vector 方法级 synchronized:读读也互斥,并发度被压到 1
- CopyOnWriteArrayList:读无锁;写复制新数组(写贵)——读多写极少场景碾压
- 详见 Vector "为什么现在基本不用"节
Set 族
Q5:Set 怎么保证元素唯一?
- HashSet:先
hashCode()定桶,再equals()确认——两者必须一致重写(契约:equals 相等则 hashCode 必须相等) - TreeSet:只看
compareTo()/compare()返回 0——与 equals 不一致时会"存进 equals 不等的对象" - 详见 00-Set总览 + Object 通用方法
Q6:HashSet、LinkedHashSet、TreeSet 怎么选?
- 默认 HashSet(最快);要保插入序 LinkedHashSet(链表微开销);要排序/范围查询 TreeSet(O(log n),禁 null)
- LinkedHashSet 本质 = HashSet + 双向链表(底层 LinkedHashMap)
- 详见 00-Set总览
Queue 族
Q7:Queue 的 add/offer、remove/poll、element/peek 有什么区别?
- 两套风格:抛异常(add/remove/element) vs 返回特殊值(offer/poll/peek,队空返回 null/false)
- 日常推荐 offer/poll/peek——判返回值比抓异常自然
- 详见 00-Queue和Deque总览 两套方法表
Q8:为什么栈要用 ArrayDeque 而不是 Stack?
- Stack 继承 Vector:粗粒度锁 + "继承了 Vector 的 15 个方法",语义污染(能从中间插)
- ArrayDeque:循环数组,头尾均摊 O(1),无锁开销,官方 Javadoc 推荐替代
- 详见 ArrayDeque + 00-Queue和Deque总览 告诫块
Q9:PriorityQueue 的原理?Top K 怎么做?
- 二叉最小堆(数组存储):offer 上浮 O(log n)、poll 下沉 O(log n)、peek O(1)
- Top K:维护大小为 K 的小顶堆,遍历比堆顶大就替换——O(n log K)
- 遍历它不是有序的(只是堆序);要有序得逐个 poll
- 详见 PriorityQueue
Map 族(重灾区)
Q10:HashMap 的底层结构?JDK 8 做了什么优化?
- JDK 7:数组+链表;JDK 8:数组+链表+红黑树——链表长度 ≥8 且容量 ≥64 树化,退化阈值 6
- 为什么阈值 8:泊松分布下链表到 8 的概率约千万分之一,树化是兜底不是常态
- hash 扰动:高 16 位异或低 16 位,减少"高位差异被 (n-1)&hash 丢失"的碰撞
- 详见 HashMap / HashMap源码解析
Q11:HashMap 扩容流程?容量为什么是 2 的幂?
- 负载因子 0.75(时间/空间折中),超过阈值扩 2 倍
- JDK 8 优化:rehash 不重算 hash——新位置要么原下标,要么原下标+旧容量(看新增的那一位 hash 是 0 还是 1)
- 容量 2 的幂让
(n-1) & hash等价取模且均匀 - 详见 HashMap源码解析 扩容节
Q12:HashMap 线程安全吗?会出什么问题?
- 不安全。JDK 7 头插法扩容并发时可成环死循环;JDK 8 改尾插解决了成环,但仍有数据覆盖丢失——不安全就是不安全
- 正解:
ConcurrentHashMap(JDK 8 CAS+锁单桶) - 详见 HashMap源码解析 线程安全节
Q13:HashMap 允许 null 键吗?为什么 Hashtable 不允许?
- HashMap 允许 1 个 null 键(固定放 0 号桶)+ 多个 null 值
- Hashtable/TreeMap 不允许 null 键:前者多线程下 get 返回 null 无法区分"不存在/值为 null";后者 null 无法参与比较
- 详见 00-容器总览 Hashtable 节
Q14:LinkedHashMap 怎么实现 LRU?
- 构造传
accessOrder=true→ 每次 get 把节点移到链尾;重写removeEldestEntry()让最老的(链头)自动淘汰 - 三行核心代码就是面试标准答案——不用手写双向链表
- 详见 LinkedHashMap
Q15:TreeMap/TreeSet 的排序依据是什么?和 equals 冲突会怎样?
- 只看
compareTo/compare是否为 0;为 0 即视为"同一元素" - 若比较逻辑与 equals 不一致:TreeMap 里会出现 equals 不等但存不进去的元素——"一致性问题"
- 详见 TreeMap + 红黑树
收尾三问(综合题)
Q16:HashMap、LinkedHashMap、TreeMap 三者怎么选?
- 默认 HashMap;要插入/访问序或 LRU → LinkedHashMap;要按键排序/范围查询 → TreeMap(代价 O(log n) + 禁 null 键)
Q17:自定义类做 HashMap 的 key 要注意什么?
- 必须同时重写
equals()和hashCode()且逻辑一致;推荐用不可变类(String/Integer)做 key——key 可变会导致哈希漂移、再也取不回来
Q18:遍历 Map 有几种方式?哪种最好?
- entrySet(键值都要,一次遍历)/ keySet / values / forEach。键值都要时 entrySet 最优(keySet 再 get 是二次查找);JDK 8 起 forEach 最简洁
- 详见 00-Map总览 上手示例节
⬅️ 00-容器选型总对比 🏠 00-Java ➡️ 00-Java并发总览
💬 评论