今年春招那段时间不少读者跑来问我同一个问题有份Java集合框架65道面试题的清单到底该怎么刷有人对着题号挨个背有人只挑HashMap的题有人刷完两遍还在ArrayList和LinkedList上栽跟头。我把这份清单从头到尾过了一遍结合自己这些年面试别人和被别人面试的经验把我认为真正值得下功夫的考点、最容易踩的坑、以及答题时怎么讲才能让面试官眼前一亮的思路整理成下面这篇文章。这份题单适合谁一种是刚开始准备校招、基础还不牢的同学可以用它做查漏补缺的索引另一种是有两三年经验、想跳槽往高级岗走的开发你需要关注的不是题目本身而是题目背后那一层源码逻辑和设计取舍。本文不打算一行一行翻译所有答案而是按从高频到冷门、从原理到坑的顺序把65道题拆成六大板块来讲清楚。1. 面试官出这65道题真正想考你的三件事1.1 集合框架的本质一套数据结构还是一份工程化设计很多同学把集合框架当成数据结构题去准备链表、哈希表、树搞得门清但面试官问为什么JDK要设计List、Set、Map三个顶层接口、为什么不直接用数组这类问题时还是会懵。原因在于集合框架本质上是一套覆盖了工程常见场景的数据结构工具箱它解决的不只是存取快不快还包括是否支持重复元素、是否要求有序、是否能并发修改、能否在遍历时删除这些工程问题。理论数据结构课讲究时间复杂度模型面试题则偏向什么时候用哪个实现类。比如时间复杂度数组的随机访问是O(1)链表是O(n)这个谁都会背但问到ArrayList插入元素为什么不一定比LinkedList慢很多人就答不上来了。原因很简单ArrayList的System.arraycopy是底层批量内存拷贝在数据量几百到几千这个区间里它往往比LinkedList逐个new Node要快复杂度分析在这个量级下是失效的。这种题想考的就是你有没有真正在业务里观察过数据结构的实际行为而不只是背结论。我面试时问过一个候选人如果我要维护一个有序的用户列表新增操作很频繁用什么他答了TreeSet我再追问那用户有重名怎么办他卡住了。其实答案是什么不唯一但你必须意识到这个问题本来就在考你对有序、可重复、CRUD频率这三个约束的权衡能力这就是集合框架的工程属性。1.2 65道题的隐藏比例基础结构、Map、并发、遍历四块把65道题按考察方向拆开你会发现命题人其实有很明确的侧重。以我经手的题单和多年的面试观察来看大约有20道集中在Collection接口下的List和Set25道左右围绕Map展开其中HashMap独占大头还会附带TreeMap和LinkedHashMap的对比剩下15道左右覆盖并发集合与迭代器其余的零散分布在排序、比较器、Collections工具类和历史集合类上。这个比例透露了一个信息面试官对HashMap的偏爱是压倒性的因为它能同时考到哈希算法、内存布局、扩容策略、树化退化、并发安全五个层级一道题就能筛出你处于哪个水平。如果你只有一周时间准备先打透HashMap性价比最高如果时间充裕再按Collection → Map → 并发 → 工具类的顺序去补全整个地图。2. ArrayList扩容公式、Set去重陷阱、LinkedList的真实用武之地2.1 ArrayList扩容数学公式怎么算面试时怎么答ArrayList是Java面试里必问的第一道菜最常见的问题是说一下ArrayList的扩容机制。及格水平的回答是默认容量10当add元素超过容量时oldCapacity右移一位加上自身即1.5倍扩容以Arrays.copyOf把旧数组内容搬到新数组。优秀水平的回答还得带上两个细节。第一个细节是扩容时的容量计算公式newCapacity oldCapacity (oldCapacity 1)右移一位就是除以2所以是1.5倍。为什么选1.5而不是2因为如果扩成2倍虽然整体搬运次数会减少但空间浪费严重1.5倍在时间和空间之间取了一个折中。你甚至可以提一下如果构造时能预估大小用new ArrayList(expectedSize)java.util.ArrayList并没有真正帮你做按预估值精准分配——扩容公式在addAll时用的是Math.max(实际需要的最小容量, 原容量 * 1.5)如果你传入expectedSize 10万它并不会直接给你开10万的数组而是只要原数组1.5倍够用就按1.5倍来。第二个细节是大批量添加时的性能陷阱。你往ArrayList里addAll一个10万元素的集合它会先扩容到15万再把10万搬过去导致有5万的闲置容量一直占着内存。对内存敏感的业务add完记得trimToSize。这里我建议你答一个负面经验某系统里我见过有人反复对一个大ArrayList调用remove(0)结果耗时从几十毫秒涨到几百毫秒这就是因为remove(0)每次都要整体前移复杂度是O(n)循环下来成了O(n²)这是ArrayList最典型的滥用场景。2.2 HashSet去重的真相equals和hashCode的约束可变对象的坑HashSet去重是另一道高频题。表面答案是HashSet通过hashCode定位桶如果桶内已有元素再通过equals比较都相同就认为是重复元素不再插入。但出题人喜欢连着问一句如果我把一个对象放进HashSet后修改了它的hashCode相关字段会发生什么答案是这个对象会驻留在错误的桶里再也无法被get和remove命中造成内存泄漏式的问题。严谨一点的表述是HashSet底层就是一个HashMap放入的元素作为key统一的PRESENT对象作为value。当元素的hashCode计算字段被改动HashMap的位置索引已经失效但这个旧引用还挂在桶链表/红黑树中。官方文档明确说必须谨慎地避免可变对象作为键。我答这道题的时候习惯补一句高端操作如果确实有这种需求要么把参与hashCode的字段设计成final要么在修改字段前先从集合里remove出去改完再加回来。另外很多面试题会问HashSet判断两个不同对象相等需要重写哪些方法。答案不是只重写equals而是equals和hashCode必须同时重写并且遵循equals相等hashCode一定相等的约定。反过来不成立——hashCode相同equals可以不等这在哈希冲突时本来就会发生。答到这里可以举一个实际案例某业务代码把BigDecimal作为HashSet的元素2.0和2.00的equal比较是true的如果BigDecimal用的是new BigDecimal(2.0)和new BigDecimal(2.00)它们的hashCode可能不一样去重就会失效。这个坑在我接触的项目里真实出现过。2.3 LinkedList被高估也被低估它适合什么场合LinkedList的面试题常规套路是ArrayList和LinkedList的区别。绝大多数人回答的是数组 vs 双向链表随机访问 vs 插入删除。这个答案只能拿到及格分因为它在多数真实场景下是错的——基于内存拷贝的ArrayList在做尾插和指定位置插入但不触发扩容时实际性能常常优于LinkedList而LinkedList的add在指定下标时也需要先遍历到那个位置一样是O(n)没有优势。那LinkedList到底什么时候赢当你的业务是高频的队头出队和队尾入队也就是它从头尾两端操作只需要O(1)指针修改不需要移动数组元素时它确实有不可替代的优势。此外LinkedList实现了Deque接口本质上是Queue和Stack的双重替代品java.util.ArrayDeque在大部分场景下比它更快基于循环数组。所以真正让LinkedList不可被替代的不是列表而是双端队列。你可以用这个角度回答LinkedList还有什么用处面试官会对你记忆更深刻。3. HashMap八连问从哈希散列到红黑树一道题吃透整条链路3.1 哈希函数的设计为什么要异或hashCode的高低位HashMap的第一问通常是HashMap的哈希函数怎么设计。源码很简短static final int hash(Object key) { int h; return (key null) ? 0 : (h key.hashCode()) ^ (h 16); }这行代码的意思是把key的hashCode高16位和低16位做异或再作为寻址的输入。为什么要这么干因为HashMap计算桶下标用的是(n - 1) hashn是数组长度默认是2的幂次。如果数组长度是16二进制就是0000...00001111那么hash值的低4位决定了落在哪个桶高28位全部浪费。如果两个key的hashCode在高位不同、低位相同它们就会被映射到同一个桶形成大量冲突。通过h ^ (h 16)把高位信息折叠到低位让低位尽可能混合高位的特征从而在数组长度较小的时候也能均匀散列。这一行代码也解释了为什么HashMap要求数组长度必须是2的幂次——用位运算替代取模前提就是n是2的幂这样(n-1)hash等价于hash%n且更快。这里我建议你主动补一句Java 17之后HashMap引入了Key自制哈希校验的增强默认依然走上面的逻辑。3.2 两个关键阈值0.75、8和64组合起来怎么讲第二问是加载因子为什么是0.75链表什么时候转红黑树为什么是8。0.75的官方注释说的是在时间和空间成本之间提供良好的权衡。讲得再透一点加载因子越大空间利用率越高但冲突概率增加加载因子越小冲突减少但数组稀疏浪费内存。0.75是泊松分布推导出的经验值配合默认容量16也就是说HashMap在元素个数达到12时会触发首次扩容。链表转红黑树的阈值是8但有前提判断条件是链表长度达到8 数组长度达到64。如果链表长度到8但数组长度不足64优先扩容而不是树化因为扩容会让链表被拆散。为什么是8源码注释给出了一个泊松分布的推演在随机哈希下链表长度达到8的概率只有千万分之六几乎永远不会发生。也就是说一旦你真的见到链表长度超过8说明hash函数设计有问题或者被恶意构造了哈希碰撞这时候才有必要用红黑树来对抗O(n)退化。第三问会顺着往下走红黑树什么时候退化成链表。答案是当树的节点数小于等于6时会从红黑树退化为链表。这里注意6和8之间有一个差值2这是为了留缓冲避免元素在7和8之间频繁震荡时反复树化和退化。我在项目里真见过有人写代码让Map不停put/remove导致链表和树来回切换性能一落千丈——这个机制就是为了防这个。3.3 扩容机制rehash全程和并发下的死循环HashMap的扩容是第四问需要讲清楚三个点什么时候扩容、扩多大、旧元素怎么搬。什么时候扩容就是元素个数超过capacity * loadFactor 12。扩多大固定的2倍16变32变64。旧元素怎么搬不是直接复制数组而是重新计算索引因为新数组长度翻倍(n-1)hash的掩码多了一个bit所以每个旧桶的元素要么留在原下标要么挪到原下标 旧容量的位置。这里有个进阶考点JDK 8在扩容时对链表采用了尾插法防止JDK 7头插法带来的死循环。经典的问题场景是多线程并发put导致某个桶的链表形成环下一次get这个桶会进入无限循环。这是老版本HashMap在国际面试题里的名场面但如果你直接说因为用了头插法所以会死循环还不够。更标准的回答是JDK 7的头插法在并发rehash时两个线程同时操作同一个链表会产生环状引用JDK 8改成尾插法后即使没有加锁也不会形成环但数据仍会丢失所以不意味着线程安全。要答到这一层面试官才会相信你不是背稿子而是真读过源码里那段rehash的实现。3.4 HashMap家族横向对比Hashtable、TreeMap、LinkedHashMap第五问到第七问通常会演变成一个对比题问法像HashMap和Hashtable的区别、什么时候用TreeMap、LinkedHashMap怎么保持顺序。我整理过一张高频的对比结论特性HashMapHashtableTreeMapLinkedHashMap有序性无序无序按key自然序或Comparator排序按插入顺序或访问顺序允许null键允许不允许不允许依赖比较器允许底层结构数组链表红黑树数组链表红黑树数组链表红黑树双向链表线程安全否synchronized加锁否否初始容量1611无固定16常用场景通用键值存储基本被淘汰的兼容类需要范围查找/排序LRU缓存、需要保持顺序的场景TreeMap值得单独花一点篇幅因为它经常被误当成有顺序的Map很多人直接回答TreeMap按key排序这没错但更值钱的理解是TreeMap实现了NavigableMap支持subMap、headMap、tailMap这些范围查询时间复杂度都是O(log n)。如果你的场景是要实时给出某个时间区间内的所有订单TreeMap比先排序再过滤的ArrayList方案要高效得多。LinkedHashMap的考点集中在accessOrder这个参数构造时传入true会开启访问顺序配合removeEldestEntry重写可以轻松手写一个LRU缓存。面试官问Redis的LRU怎么实现你答用LinkedHashMap的accessOrder模式原理类似也可以用它做考点推导立刻就能拉高档次。4. 并发集合四件套Vector、Hashtable、CopyOnWriteArrayList、ConcurrentHashMap4.1 从全表锁到分段锁并发容器演进里藏着设计思路65道题里至少有四五道涉及线程安全的集合类而且问法是递进的。第一层是Vector和Hashtable为什么慢第二层是CopyOnWriteArrayList的原理第三层是ConcurrentHashMap为什么比Hashtable快第四层是BlockingQueue的应用场景。想把这四层串起来回答你需要抓住一条主线锁粒度从大到小的演进。Vector和Hashtable的年代实现线程安全的方式最粗暴——在方法签名上加synchronized等同于给整个对象加锁。任何线程调用方法都必须先获取对象锁即使只是访问不同桶的元素也要互斥等待并发度几乎等于0。这种方案叫全表锁在低并发时代尚可接受一旦并发量上来锁竞争会让吞吐量直线下降。ConcurrentHashMap在JDK 7引入了分段锁将数组逻辑分成16段操作落在同一段才需要竞争锁不同段可以并行访问锁粒度下降了并发度提高了16倍。JDK 8更进一步虽然写法是synchronized CAS但锁的对象从段细化到了单个桶的头节点只有两个线程同时操作同一个桶才会互相等待。这一演进过程几乎就是面试官想听的内容你能把锁粒度这个维度贯穿到所有并发集合问题里答题水平会明显高于只说Hashtable是同步的所以慢的候选人。4.2 ConcurrentHashMap的写操作CAS和synchronized怎么配合如果你能讲到源码层面面试官多半会追问ConcurrentHashMap的put流程。标准的拆解顺序是先用(n-1)hash定位到桶如果桶为空用CAS尝试把新Node放进桶这一步不需要加锁如果CAS失败则说明桶非空进入synchronized块锁住这个桶的头节点再走链表或红黑树的插入逻辑插入后如果链表长度达到8且数组长度达到64尝试转换成红黑树最后检查整个map的元素数量超过阈值则扩容JDK 8的扩容支持多线程协助迁移旧桶也就是sizeCtl由负数触发各线程认领区间完成rehash。这里有个容易漏的细节为什么CAS只用于桶为空的情况而不用在整个put流程里因为CAS只能保证单点更新的原子性链表插入涉及改next指针维护size多个步骤无法靠一次CAS完成必须用锁把这段临界区保护起来。把这句话说出来面试官会知道你对CAS的适用边界有真实的掌握而不是背了网上那句CASsynchronized就以为自己懂了。4.3 CopyOnWriteArrayList的读写分离与BlockingQueue的三种缓冲语义CopyOnWriteArrayList是面试题里的另一个常客它解决的是读多写少的场景。读的时候不加锁直接读底层的volatile数组写的时候复制出一个新数组在新数组上修改再用volatile引用替换旧数组。这样读线程永远不必等待写线程但代价是每次写都要复制整个底层数组如果数据量大、写频繁GC压力和内存开销会非常恐怖。这道题的隐藏考点是读到的数据可能不是最新的。因为在替换数组的瞬间可能有线程已经持有了旧数组的引用它会继续读旧数据。对这个现象你的标准话术是CopyOnWriteArrayList提供的是弱一致性它只保证最终读到某一时刻的完整快照不保证实时一致。如果面试官拿这个反问那它算线程安全吗你要能回答线程安全不等于强一致它ArrayIndexOutOfBoundsException是不会发生了但可见性延迟是设计选择不是Bug。这个回答在真实的资深岗位面试里很加分。BlockingQueue的题更贴近实际工程。ArrayBlockingQueue基于循环数组take和put用同一把锁LinkedBlockingQueue在JDK里默认无界生产环境必须传入capacity限制否则生产者暴涨会拖垮内存SynchronousQueue不存储元素每个put必须直接匹配一个take适用于零缓冲的直传模型。这三种队列语义正好对应线程池的三种任务缓冲策略有界队列能限流无界队列在流量小的时候业务简单SynchronousQueue适合不排队、来了就执行的场景。回答的时候把队列和线程池的拒绝策略挂上钩比单纯背诵接口方法要有效得多。5. 遍历和比较器里的失分区ConcurrentModificationException、Comparable与Comparator5.1 fail-fast机制迭代器为什么不让你在遍历时直接remove面试题里有一道经久不衰的陷阱题如下代码会抛什么异常为什么怎么解决ListString list new ArrayList(); for (String s : list) { if (s.equals(a)) { list.remove(s); } }答案是抛出ConcurrentModificationException原因在于ArrayList的迭代器内部维护了一个expectedModCount字段每次调用next()时都会检查它是否等于ArrayList的modCount。modCount每次结构性修改add、remove、clear都会自增而上面的代码通过list.remove修改了list迭代器里的expectedModCount却没有同步更新于是两次数值不一致迭代器就判定发生了并发修改立刻抛出异常。注意这个检查只在next和remove方法里做如果修改发生在迭代结束后就不会抛异常这也是为什么有的代码偶尔不报错——它不是没修改而是还没来得及检查到。这个机制的学名叫fail-fast事件上它保护的是程序的确定性宁可快速抛错暴露问题也不要脏数据一路跑到系统深处。解决方案是调用迭代器自己的remove方法因为迭代器每次remove之后会同步更新expectedModCount。我来面试的时候还会提醒一句:forEachRemaining是Java 8之后的迭代器方法它同样会触发fail-fast检查千万别在lambda里对集合做结构性修改。5.2 Comparable与Comparator排序规则的两种设计选择Comparable和Comparator的区别这个问题的标准回答有三个要点。第一Comparable定义在类内部意味着这个类天生具备一种排序方式compareTo是它的自然排序Comparator定义在外部意味着排序规则独立于类的实现可以在不修改类代码的情况下为同一个类定义多种排序规则。第二使用习惯上Collections.sort(list)依赖元素自身实现ComparableCollections.sort(list, comparator)则是把排序规则以参数形式传入。第三有一个容易忽略的约束如果同时存在两者Comparator显式传入时会覆盖自然排序。这题要答出高级感可以加一段实际踩坑经历。比如某系统里用户对象实现了Comparablerule是按年龄升序后来产品需求变了要在某些界面按最后登录时间降序。如果直接改compareTo依赖自然排序的所有逻辑都会跟着变很容易误伤。正确做法是保留自然排序为最后登录时间新建一个Comparator。这道题给你上的价值课是Comparable本质上是领域内最被认可的默认顺序不适合频繁变动Comparator才是应对多变业务规则的利器。5.3 迭代器里的removal和sort排序的稳定性sorted在非稳定排序的语境里还有一道衍生题Collections.sort用的是稳定排序吗ArrayList里的sort走的是Arrays.sort对对象数组采用TimSort是稳定排序。TimSort的核心是识别出数组里已天然有序的片段run然后把这些run合并起来。它的复杂度在最坏情况下是O(n log n)在近乎有序时能做到接近O(n)。回答的时候把稳定排序的定义相等元素的相对位置不变和TimSort能兼顾随机数据和部分有序数据这两个特性讲出来这道题就算答圆满了。另外Java 8的Stream的sorted方法也是稳定排序适合做先按A排序再按B排序的多级排序场景只要按B先sort再按A sort一次整个结果就会在同一A值内保持B的相对顺序。这种用法在写报表排序的时候很实用值得在回答稳定排序有什么应用时举出来。6. 题单之外源码阅读方法和刷题策略的实战建议6.1 怎么把看过源码从简历加分项变成面试保命题很多面试题最终会落脚到你有没有读过源码。这里的分寸很重要。如果你的真实经验只有看过transferFrom的消息通知在面试时一定要说清楚自己看到哪一层。比如HashMap你可以说我读完了put、get、resize和treeifyBin这几个方法然后当场复述一下treeifyBin的触发条件链表长度8 数组长度64否则走resize——这就够了。最怕的是在简历上写精通HashMap源码结果连resize里链表拆分是按(e.hash oldCap)判断都说不出来那样反而会直接送掉offer。如果你还没有完整读过源码我给你一条务实的路线先读ArrayList和LinkedList的add/remove建立线性结构的直觉接着读HashMap的hash、put、resize把哈希和扩容的完整链路打通然后读ConcurrentHashMap的put方法顺着那两三个synchronized块理解锁粒度最后读CopyOnWriteArrayList的add操作理解写复制机制。这四段读透题单里60%以上的源码类题你都能从容应对。6.2 65道题的推荐刷题顺序与时间分配刷题顺序比刷题数量更重要。我建议把65道题分成三轮。第一轮只做输出型对着题单的题目不看答案用30秒口头讲一遍思路卡住的地方直接标记这一轮的作用是暴露盲区。第二轮做深化型对高频题ArrayList扩容、HashMap的put、fail-fast、Comparable与Comparator不只是讲思路还要能写出关键的伪代码或源码片段。第三轮做综合型把相关联的题串成链条来答比如从HashMap为什么线程不安全串到ConcurrentHashMap怎么解决再从ConcurrentHashMap串到CopyOnWriteArrayList怎么工程化取舍把零散知识点变成网状结构。时间分配上如果总共有两周我建议第一周按照基础结构40%、Map 60%第二周按并发集合40%、遍历排序30%、工具类30%来安排。不必一味追求全部背熟而是要确保自己熟悉到能指导别人怎么写代码的程度——面试官判断你懂不懂往往不是看你答对了多少而是看你在讲到某个机制时会不会主动把为什么会这样设计的下半句说出来。6.3 一道题从能答到让面试官记你三步加长法最后分享一个我自己多年面试下来觉得最有效的答题技巧三步加长法。第一步先给出精确到关键数字的答案比如默认负载因子0.75容量16扩容阈值12。第二步给你的答案补一点设计初衷比如0.75是空间和时间的权衡避免过早扩容导致内存浪费也避免过晚扩容导致冲突密集。第三步给一个工程上的延伸比如如果我能预估元素量是2万我会直接用new HashMap(20480)来初始化减少扩容带来的复制开销。这个方法在HashMap这种题目上效果非常明显。很多人在第一步就停住了面试官听完内心平静能做到第二步的已经超过了大多数人能稳做第三步的面试官就会在评价里写有工程经验、有设计意识。这65道题里凡是涉及到为什么默认值是多少什么场景用哪个的都值得照这个三步法过一遍。你不需要等面试官挖你挖是挖不到亮点的——亮点要你自己主动给出去。
阅读完成 · 觉得有帮助?