首页 / Java 后端八股精讲 / 附录
附录速查与总索引
这里放三类东西:已经压缩到极限的复习材料(A.1 / A.2)、 面试现场用得上的表达工具(A.3 / A.4)、以及卡住时的求助路径(A.5)。 注意:A.1 的定位是「复习」不是「初学」——没读过正文就直接看表,等于只背了结论。
A.1四槽速记卡总表
一行一条,斜体追问是「最可能被接上的下一句」。复习时盖住后两列,只看问法自述。
| # | 八股问法 | 一句话答案 | 追问 |
|---|---|---|---|
| 1 | 为什么工程里几乎不用 LinkedList | 复杂度一样但常数差一个量级:数组连续能命中缓存行。 | ArrayList 为什么扩 1.5 倍? |
| 2 | HashMap 的 get 为什么平均 O(1) | 位运算算出桶下标直接寻址,桶内链表/红黑树兜底冲突。 | 最坏情况呢? |
| 3 | 容量为什么必须是 2 的幂 | n-1 二进制全 1,与运算才不浪费桶、等价于取模。 | 那还要扰动函数做什么? |
| 4 | 负载因子为什么是 0.75 | 空间与冲突的交点;再大冲突陡增,再小频繁扩容。 | 扩容时元素怎么迁移? |
| 5 | JDK 7 扩容为什么会死循环 | 头插法反转链表,并发迁移时形成环,get 时无限循环。 | JDK 8 就安全了吗? |
| 6 | 树化阈值为什么是 8 和 6 | 8 来自泊松分布(概率约 6e-8);6 与 8 留空隙防抖动。 | 容量不够 64 时呢? |
| 7 | ConcurrentHashMap 怎么保证线程安全 | JDK 8:空桶 CAS、非空桶 synchronized 锁桶头。 | size() 怎么数准? |
| 8 | 它为什么不允许 null 键和 null 值 | 并发下无法区分「不存在」与「值是 null」,二义性会埋雷。 | 那 HashMap 为什么允许? |
| 9 | Java 是值传递还是引用传递 | 只有值传递;对象传的是引用的副本,能改内部状态、不能让实参改指向。 | C++ 为什么能写 swap? |
| 10 | String 为什么设计成不可变 | 常量池才能共享、hashCode 才能缓存、才能安全跨线程传递。 | new String("abc") 创建几个对象? |
| 11 | 重写 equals 为什么必须重写 hashCode | HashMap 先按 hashCode 找桶、再按 equals 认人,少一个就取不回来。 | equals 的参数为什么必须是 Object? |
| 12 | 什么是类型擦除 | 泛型只在编译期生效,编译后擦到上界,运行时不保留具体类型。 | Spring 怎么拿到泛型类型? |
| 13 | Integer 128 == 128 为什么是 false |
只有 -128~127 走 IntegerCache,超出范围各自 new,比的是地址。 | 那包装类怎么比值? |
| 14 | 类的初始化顺序 | 父静态 → 子静态(只一次)→ 父实例块 → 父构造 → 子实例块 → 子构造。 | 静态常量会触发类初始化吗? |
| 15 | 递归多深会栈溢出 | 默认栈 1MB,实测约 2.3 万层;帧越大越浅(多 3 个 long 掉到 7360)。 | 溢出了能 catch 吗? |
| 16 | new Object() 占多少字节 |
16 字节:头 12(Mark Word 8 + 类型指针 4)+ 4 对齐填充。 | 为什么头是 12 不是 8? |
| 17 | 双亲委派为什么要打破 | Tomcat 要应用隔离、JDBC 要跨加载器找驱动,都靠重写 loadClass 或 TCCL。 |
Bootstrap 在 Java 里是什么? |
| 18 | 线上 OOM 怎么排查 | 先看报错文本判断爆哪个区,再看 Full GC 后老年代降不降,定泄漏还是不够。 | 没 dump 怎么办? |
| 19 | 为什么用 B+ 树不用二叉树 | 瓶颈是磁盘 IO:一页放 1638 个指针,3 层撑到亿级,IO 只 3 次。 | 3 层到底能存多少行? |
| 20 | 什么是回表 | 二级索引叶子只有「索引列 + 主键」,拿到主键还得去聚簇索引再查一次。 | 怎么避免? |
| 21 | 最左前缀为什么成立 | 索引先按第 1 列排、相同再按第 2 列排,所以要用第 N 列,前 N-1 列必须等值。 | 范围条件放前面会怎样? |
| 22 | 深分页怎么优化 | 延迟关联(覆盖索引取 id)或游标分页(where id > lastId),后者不支持跳页。 |
为什么延迟关联就快了? |
| 23 | 三个现象怎么一句话分清 | 脏读=读到未提交;不可重复读=值变了;幻读=行数变了。 | 为什么幻读锁行解决不了? |
| 24 | MVCC 靠什么实现 | 三个东西:隐藏列(DB_TRX_ID/DB_ROLL_PTR)、undo 版本链、ReadView。 |
ReadView 里存的是数据吗? |
| 25 | RC 与 RR 的差别在哪 | 只有一个:ReadView 的创建时机。RC 每条 select 建一次,RR 事务首次快照读建一次并复用。 | 那 RR 为什么还要间隙锁? |
| 26 | RR 到底防不防幻读 | 快照读防住了(MVCC),当前读要靠间隙锁;先快照读后当前读,仍会看到新行。 | 记录锁、间隙锁、next-key 什么关系? |
| 27 | for update 为什么要走索引 | 不走索引要全表扫描,扫到的每行都加锁,行锁退化成表锁,还带上 supremum 伪记录。 | 线上遇到这种事故怎么快定位? |
| 28 | 1213 和 1205 有什么区别 | 1213 死锁,整个事务被回滚;1205 锁等待超时,只回滚当前语句。 | 哪一种更危险? |
| 29 | 线程池提交任务时的扩容顺序 | 核心 → 队列 → 扩容到 max → 拒绝。core=2/max=4/queue=2 时第 3 个进队列、第 5 个才扩容、第 7 个被拒。 | 队列是无界的会怎样? |
| 30 | 为什么禁用 Executors 的工厂方法 | newFixedThreadPool 用无界队列,任务堆积到 OOM 且 max 永不生效;newCachedThreadPool 的 max 是 Integer.MAX_VALUE,线程会爆炸。 |
那线程数该开多少? |
| 31 | 线程数怎么定 | CPU 密集 N+1,IO 密集 N×(1+W/C)。公式只是起点,上线看活跃线程数与队列长度再调。 |
开多了为什么反而慢? |
| 32 | AQS 到底是什么 | 一个 volatile int state + 一个 CLH 双向等待队列。Lock 里 state 是重入次数,Semaphore 是许可数,Latch 是剩余计数。 |
能看见这个队列吗? |
| 33 | count++ 为什么不是原子的 |
编译成「读 → 加一 → 写回」三条指令。实测 8 线程各 10 万次,丢 3617 次(约 0.45%)。 | 换成 Atomic 就一定好吗? |
| 34 | ThreadLocal 为什么要 remove() |
线程池复用线程,ThreadLocalMap 跟着复用 → 后一个请求读到前一个请求的用户。key 是弱引用但 value 是强引用 → 还泄漏。 |
不 remove 一定出问题吗? |
| 35 | Bean 生命周期的十个回调 | 构造 → 注入 → Aware → BPP.before → @PostConstruct → afterPropertiesSet → initMethod → BPP.after → @PreDestroy → destroy → destroyMethod。 |
BPP.after 为什么在 initMethod 之后? |
| 36 | 三级缓存各存什么 | 一级成品、二级半成品、三级 ObjectFactory。第三级只为推迟「要不要包代理」的决定,让代理在早期就生成好且只生成一次。 |
两级为什么不行? |
| 37 | Spring 能解决哪种循环依赖 | 只有单例 + setter/字段注入。构造器(对象还不存在)、原型(没有单例池)全部启动失败。 | Boot 2.6 之后还默认允许吗? |
| 38 | @Lazy 是怎么断环的 |
注入的是 CGLIB 代理(类名带 $$SpringCGLIB$$),第一次调用才去容器取。把启动期错误推迟成运行期错误。 |
那生产上该用吗? |
| 39 | 原型 Bean 会被销毁吗 | 不会。实测两次 getBean 后 close(),@PreDestroy / DisposableBean / destroyMethod 零次执行。 |
那连接谁来关? |
| 40 | 两种代理怎么选 | 有接口 → JDK $Proxy(兄弟);无接口 → CGLIB $$SpringCGLIB$$(父子)。final 类/方法、private 方法拦不到。 |
代理一次多花多少? |
| 41 | @Transactional 失效的六种 | 自调用、private、final、类不归 Spring 管、异常被吞、表不支持事务。前四条同一个原因:没走代理。 |
怎么一眼确认走了代理? |
| 42 | 什么异常才回滚 | 默认只有 RuntimeException / Error。受检异常要 rollbackFor;实测抛 Exception 时行数=1(没回滚)。 |
catch 住又不想留脏数据? |
| 43 | REQUIRED 内层失败为什么连累外层 | 内层加入外层事务,抛异常时整个事务被标 rollback-only;外层 catch 后提交 → UnexpectedRollbackException。 |
换成 REQUIRES_NEW 呢? |
| 44 | timeout 与事务成本 | timeout 落到 Statement.setQueryTimeout:sleep 3s 不超时,select sleep(3) 1.03s 就抛。一次事务约 0.34 ms。 |
一万条循环插入要多花几秒? |
A.2与算法专题的对照总索引
这张表是两套教程的「接线图」。左边是算法专题,右边是对应的八股主题。 刷到哪个专题,就顺手把右边那几条读了——这是把两套知识缝起来成本最低的方式。
| 算法专题 | 对应八股主题 | 缝在一起的那句话 |
|---|---|---|
| 02 · 哈希表 | 01 · 集合框架 | 两数之和的 O(1) 查找,就是 (n-1)&hash 那一次寻址。 |
| 05 · 链表 | 01 · 集合框架 | 链表题在练指针,工程里却几乎只用 ArrayList——差在缓存行。 |
| 14 · 位运算 | 01 · 1.4 寻址 | 哈希与位运算是同一件事:用掩码取低位代替取模。 |
| 21 · 字符串 | 02 · 2.3 String 不可变 | String 不可变 + 常量池,决定了为什么拼接要用 StringBuilder。 |
| 03 · 滑动窗口 | 02 · 2.3 String 不可变 | 窗口里用 result += c 会退化成 O(n²)——每次拼都新建对象。 |
| 02 · 哈希表(自定义 key) | 02 · 2.4 equals 与 hashCode | 字母异位词分组把「排序后的字符串」当 key,靠的就是这两个方法的契约。 |
| 08 · 递归与回溯 | 03 · 3.3 栈帧与递归深度 | 回溯的每一层就是栈帧的一层;默认 1MB 栈只够两万层左右。 |
| 05 · 链表(递归反转) | 03 · 3.3 栈帧与递归深度 | 5 万节点的递归反转必炸;链表长度不是你能控制的,所以写迭代。 |
| 01 · 数组与双指针 | 03 · 3.4 对象布局 | 存 100 万个 int:数组 4MB,LinkedList 48MB——差在对象头和缓存行。 |
| 18 · 二叉搜索树 | 11 · 11.2 为什么不用二叉树 | BST 是「两叉、层数 log n」;B+ 树是「千叉、层数 3」——差别在磁盘 IO 不是比较次数。 |
| 19 · Trie 前缀树 | 11 · 11.6 索引失效 | 前缀能定位、中缀不能:这就是 like 'abc%' 走索引、'%abc' 不走的同一个道理。 |
| 07 · 二叉树(最大深度) | 03 · 3.3 栈帧与递归深度 | 树退化成一条链时,5 万个节点的递归就在 1MB 栈的上限上跳舞。 |
| 15 · 设计题(LRU) | 15 · 工程手写题(待写) | LRU = 哈希表 + 双向链表,两道八股的手写合体。 |
| 12 · 图论 | 09 · Spring 循环依赖(待写) | 三级缓存解决循环依赖,本质是图上的环检测 + 提前暴露。 |
| 12 · 图论(环检测) | 12 · 12.8 死锁 | 死锁就是「事务等待图里出现了环」,InnoDB 的检测就是找环,找到就回滚一个。 |
| 05 · 链表(遍历) | 12 · 12.3 undo 版本链 | DB_ROLL_PTR 就是版本链的 next 指针,找可见版本=顺着链往回走。 |
| 06 · 栈与队列(队列语义) | 08 · 8.2 阻塞队列与扩容 | 算法里队列是你手里的容器,线程池里它是线程之间的交接台:offer 满了阻塞、poll 空了等待。 |
| 16 · 分治(归并排序) | 08 · 8.7 LongAdder 分段累加 | 把抢同一个变量的热点拆成多个 Cell 各记各的,最后合并——和「先分再合」是同一个思路。 |
A.3画图清单:哪几题必须会画
面试里用嘴说「数组加链表加红黑树」对方很难跟上,画三笔反而更快讲清。 下面这几张图建议在纸上各练三遍,练到 20 秒内画完。
| 图 | 用在什么追问上 | 必须画出的元素 |
|---|---|---|
| HashMap 骨架图 | 「它的结构是什么」 | 一列桶数组 + 其中两个桶里挂链表 + 一个桶画成树 + 标出 16 / 0.75 |
| 一次 put 的路径图 | 「put 的流程说下」 | 算 hash → 取模定位 → 空桶放入 / 非空遍历 → 尾插 → 判断树化与扩容 |
| 扩容拆链图 | 「扩容怎么迁移」 | 一条链拆成两条,标出「原位 j」和「j + oldCap」,注明判断依据是 hash & oldCap |
| JDK 7 成环图 | 「为什么会死循环」 | 两个线程 + 一个变成 a→b→a 的环,圈出「get 落在这里就出不来」 |
画图的隐性加分项:画完主动标一下「这里是 O(1)」「这里是 O(log n)」。
面试官会立刻知道你不仅记得结构,还记得每一段的代价——这是「背过」和「想过」的分界线。
A.4追问链路:一题被追到第五层
面试的真实形态是一条向下的链,不是一张并列的表。下面这条链从一句最普通的话开始, 每一层都是上一层的自然延续——练这条链,比背八个独立问题有用得多。
| 层级 | 对方问 | 你要给出 |
|---|---|---|
| 第 1 层 | 「你这个为什么用 HashMap?」 | 因为要按 key 快速查——平均 O(1)。 |
| 第 2 层 | 「为什么能 O(1)?」 | 算 hash、用 n-1 掩码取低位定位桶,一次寻址。 |
| 第 3 层 | 「冲突了怎么办?」 | 桶内链表;长度到 8 且容量够 64 转红黑树,最坏降到 O(log n)。 |
| 第 4 层 | 「元素多了会怎样?」 | 超过容量×0.75 扩容翻倍;迁移只看 hash & oldCap 一位,原位或 +oldCap。 |
| 第 5 层 | 「并发下能用吗?」 | 不能。JDK 7 会成环,JDK 8 会丢数据;并发要用 ConcurrentHashMap(CAS + 锁桶头)。 |
JVM 这条更典型:从一个「方法跑挂了」的现象出发,五层之内会穿过栈、堆、对象布局,最后落到线上排查:
| 层级 | 对方问 | 你要给出 |
|---|---|---|
| 第 1 层 | 「这个方法递归 10 万层怎么就挂了?」 | 栈溢出(StackOverflowError),是调用太深,不是内存不够。 |
| 第 2 层 | 「栈里到底存的什么?」 | 栈帧:局部变量表、操作数栈、动态链接、返回地址。每调一次压一帧。 |
| 第 3 层 | 「那为什么不是堆溢出?」 | 对象在堆、调用在栈。一个报 StackOverflowError、一个报 OutOfMemoryError,处理方式完全不同。 |
| 第 4 层 | 「那对象在堆里占多大?」 | 头 12 字节(Mark Word 8 + 类型指针 4)+ 字段 + 按 8 对齐;new Object() 是 16。 |
| 第 5 层 | 「线上真 OOM 了你怎么查?」 | 先看报错文本定区(heap / Metaspace / native thread),再看 Full GC 后老年代能不能降——降不下是泄漏,加 -Xmx 没用。 |
同样的练法,Java 基础这条链从「一句大白话」出发,五层之内会把值传递、不可变、equals 契约全串起来:
| 层级 | 对方问 | 你要给出 |
|---|---|---|
| 第 1 层 | 「我在方法里改了对象,外面怎么也跟着变了?」 | 因为传的是引用的副本,副本和实参指向同一个对象,改的是对象内部。 |
| 第 2 层 | 「那不就是引用传递吗?」 | 不是。让形参重新指向新对象试试,外面一点不变——这正说明传的是值。 |
| 第 3 层 | 「那 String 传进去为什么改不了?」 | String 本身不可变,任何「修改」都在造新对象,和传参方式无关。 |
| 第 4 层 | 「不可变到底换来什么?」 | 常量池能共享、hashCode 能只算一次、天然线程安全;代价是拼接要造新对象,所以有 StringBuilder。 |
| 第 5 层 | 「那自定义对象当 HashMap 的 key 要注意什么?」 | equals 与 hashCode 必须一起重写、参数写成 Object,且不要拿可变字段算 hash。 |
索引这条链的起点是一句「这条 SQL 怎么这么慢」——它会在五层之内把 B+ 树、回表、最左前缀全部拉出来:
| 层级 | 对方问 | 你要给出 |
|---|---|---|
| 第 1 层 | 「这条 SQL 为什么慢?」 | 先看执行计划:type 是不是 ALL、rows 有多大、Extra 有没有 Using filesort。 |
| 第 2 层 | 「为什么没走索引?」 | 列上做运算 / 函数、隐式类型转换、左模糊 like,或者 or 另一边没索引——本质都是「列的值在比较前被改了」。 |
| 第 3 层 | 「加了索引为什么还是慢?」 | key_len 看用到了联合索引的第几列;再看 Extra 有没有 Using index(没覆盖就要回表)。 |
| 第 4 层 | 「回表为什么贵?」 | 二级索引叶子只有「索引列 + 主键」,取整行要按主键再走一次聚簇索引,是随机 IO。 |
| 第 5 层 | 「那为什么不用二叉树省得回表?」 | 二叉树 100 万节点 20 层就是 20 次 IO;B+ 树一页 1638 个指针、3 层撑到亿级,IO 只有 3 次。 |
事务这条链从「我明明提交了,他怎么还读不到」出发,五层之内会走到 ReadView 的四个数字上:
| 层级 | 对方问 | 你要给出 |
|---|---|---|
| 第 1 层 | 「为什么我提交了,他那边还读不到?」 | 他开的是可重复读,读的是快照,不是最新数据——差的是 ReadView 的时刻。 |
| 第 2 层 | 「那这个快照存在哪?」 | 没有副本。只有四个数字:m_ids、min_trx_id、max_trx_id、creator_trx_id。 |
| 第 3 层 | 「那它怎么知道该看哪个版本?」 | 顺 DB_ROLL_PTR 沿 undo 版本链往回走,拿每个版本的 DB_TRX_ID 和 ReadView 比,第一个可见的就是。 |
| 第 4 层 | 「那 RR 和 RC 差在哪?」 | 只差 ReadView 建的频率:RC 每条 select 一次,RR 整个事务复用一次。 |
| 第 5 层 | 「那同一个事务里 update 用的是旧值还是新值?」 | 新值。update 是当前读,不走 MVCC——所以 set v = v + 1 安全,先查后写不安全。 |
并发这条链从「线上接口全卡住了」出发,五层之内会落到线程池的三个数字上:
| 层级 | 对方问 | 你要给出 |
|---|---|---|
| 第 1 层 | 「线上接口全卡住了,你怎么查?」 | 先 jstack 看线程状态分布:全 WAITING 是等队列,全 BLOCKED 是抢锁,全 RUNNABLE 停在 socket 是下游超时。 |
| 第 2 层 | 「怎么确定是线程池打满?」 | 看三个数一起涨:活跃线程数 = max、队列长度持续上涨、completedTaskCount 不涨。 |
| 第 3 层 | 「为什么会打满?」 | 任务里有阻塞调用(慢 SQL / 远程接口)占了线程,或者线程数本来就配少了。前者居多。 |
| 第 4 层 | 「那多加线程不就行了?」 | 16 核实测:16 → 32 线程几乎不变(59→56ms),32 → 64 反而变慢(56→65ms),因为上下文切换。 |
| 第 5 层 | 「那到底怎么配?」 | 队列必须有界(否则 max 不生效、内存涨到 OOM),handler 用 CallerRuns 形成背压,配好线程名,再按实测调参数。 |
怎么用这六张表:找一个同学(或对着录音)从第 1 层开始问,你连续答到第 5 层不卡壳,这一条就算过了。
卡在第几层,就回去读对应那一节——这比从头再读一遍整章高效得多。
A.5卡住了去哪查
按「可信度」排序,而不是按「搜索是否方便」排序。前两个是权威源,后两个是辅助理解。
| 来源 | 性质 | 什么时候用 |
|---|---|---|
JDK 源码java.util.HashMap |
权威 | 所有「具体数值、阈值、流程」的问题。注意看注释——树化阈值 8 的泊松分布说明就写在类注释里,比任何二手资料都准。 |
| JEP / 官方文档 | 权威 | 「为什么这么设计」的问题,比如某版本为什么改实现。 |
| JavaGuide / 小林 coding | 二手,质量高 | 建立整体框架、找图。看到具体数字仍要回源码核一遍。 |
| 博客 / 面试题合集 | 三手 | 只用来「看别人怎么组织表达」,不要信其中的数字和结论——版本漂移最严重的就是这类内容。 |
一个真实存在的坑:网上大量文章写「JDK 8 之后 HashMap 就线程安全了」——这是错的。
JDK 8 只是修掉了「成环」,数据覆盖依然存在。
判断一条八股是否过期,最可靠的办法是回源码看那段逻辑还在不在,而不是看文章的发布时间。
一句话记忆(附录):复习看 A.1,缝合看 A.2,表达看 A.3 和 A.4,求证看源码——
四件事不要混着做,混着做就是「看了很多,什么也没留下」。