又是一年金三银四的面试季,我坐在面试官的位置上,看着对面候选人简历里写着"熟练掌握 Java 核心知识",于是随口问了一句"HashMap 在 JDK 1.8 里扩容时,为什么要把链表拆成高位和低位两组"。他沉默了几秒,回答说"这个我没背过"。
这一幕我见过太多次了。我自己当年也是背"java面试八股文"出身,后来做了面试官,再回头看这些题目,才意识到大多数人对常规 Java 面试题的理解从一开始就跑偏了。这篇文章不是什么面试宝典的搬运,而是我从"背题人"变成"出题人"之后,对一轮典型 Java 技术面里最常出现的那些题做的一次系统复盘。它会拆解 java基础、集合、并发、JVM、手撕算法、工程化这几个板块里真正值得花时间的点,并解释面试官为什么偏偏爱问这些。无论你是正在准备校招的应届生,还是打算跳槽的 Java 开发者,甚至只是觉得自己"基础还行"但一面试就露馅的同行,这篇文章应该都能让你少走点弯路。
1. 别把八股文当背诵比赛:先搞清楚面试官到底在问什么
1.1 一场面试下来,八股文的真实占比和权重
很多人在各类求职 App 上刷到"java面试题""java面试大全及答案"这类关键词,第一反应是收集一份汇总文档,然后开始背。我见过最夸张的一位候选人,把网上流传的两百多道题打印出来,背了将近一个月,结果面试官随口问了一句"你平时项目里什么时候会用到 volatile",他直接卡壳了。
这里有个核心问题:面试官要的从来不是标准答案,而是你的理解路径。常规 Java 面试题之所以被叫"八股文",是因为它们出现频率高、涉及面广。但在面试官眼里,这些题目更像是探针。同一道题,新人背答案和老手讲原理,说出来的内容完全不一样。你可以大胆地在简历里写"熟悉集合源码",但问到 put 方法的具体流程就露馅,这比直接承认没准备还减分。
从我面试别人的实际经验来看,大多数技术面会把 60% 到 70% 的时间放在"基础题 + 项目追问"上,其中基础题基本就是八股文范围。但关键不在于要不要背,而在于你能不能把答案组织成"是什么、为什么、适用场景、边界条件"这四层。能讲透四层的候选人,哪怕答案里有一个小错,我也会给过;背得一字不差但说不清原因的,反而让我怀疑他有没有真实编码经验。
1.2 从"背答案"到"讲原理"的认知转变
我刚开始准备面试的时候也走过弯路。当时收集了一份"java面试题大全"照着背,还觉得自己挺努力。后来有一次被问到 String、StringBuilder、StringBuffer 的区别,我按网上的标准答案背完了,面试官紧接着来了一句"那你知道为什么 String 被设计成不可变吗",我当场愣住了。
这个问题根本不用背,想清楚就能答出来。String 不可变,一是因为字符串常量池需要共享,不可变才能保证安全;二是因为 String 经常作为 HashMap 的 key,如果它可变,hashCode 就会失效,已经放入哈希表的数据就找不到了;三是因为不可变对象在多线程环境下天然是线程安全的,不需要额外同步。这三个理由,每一个都能引出下一个问题。面试官问八股文,很多时候就是为了引出这条追问链,你答得越深入,他越能判断你的真实水平。
所以后来我把复习方法改了:不再按题目背,而是按知识点画"问题树"。比如面对"集合"这个主题,先问自己 ArrayList 底层是什么、扩容怎么扩、和 LinkedList 的适用场景差别在哪、为什么会有 fail-fast 机制、ConcurrentHashMap 是怎么解决并发问题的。每答完一个,再往深处问自己一个"为什么",直到问不动为止。这个过程淬炼出来的答案,比任何八股文汇总都扎实,而且面试时不会因为题目变了个问法就断片。
1.3 常规题的四个考察维度
我把高频的常规 Java 问题归类成四个维度,后面几章会逐个展开:
- 基础语法与面向对象:考察语言基本功,包括三大特性、重载重写、equals 和 hashCode、异常体系、枚举、Lambda 等。这类题是面试开场的主力,也是很多人轻敌的地方。
- 集合框架:考察对常用数据结构的理解深度。HashMap 几乎是必问,ArrayList、LinkedList、ConcurrentHashMap 也是高频题。
- 并发与多线程:考察是否有实际处理并发问题的经验。synchronized、volatile、线程池、锁升级是重灾区,最难糊弄过去。
- JVM 与内存:考察对运行时数据区、类加载、垃圾回收和线上问题排查的掌握。OOM 系列问题经常被包装成情景题来问。
这四个维度基本就是常规 Java 面试轮的核心范围。下面我按这个顺序,把每个维度里最容易翻车、也最值得花时间的点,一个个拆开讲。
2. 面向对象和基础语法:送分题里藏着的送命题
2.1 面向对象三大特性:怎么答才算合格
"面向对象编程 Java 的三大特性是什么",这是一道幼儿园级别的题,但能把封装、继承、多态讲清楚的候选人,我这两年遇到的不到三成。问题不在他们不知道这三个词,而在表达方式。
大多数人会这样答:"封装就是把属性私有化,继承就是子类继承父类,多态就是同一个方法有不同的实现。"通篇是名词解释,没有任何信息量。合格的回答至少要带出设计意图:封装不是为了隐藏字段,而是为了控制访问边界、降低模块间的耦合,对外暴露的应该是行为而不是数据;继承的本质是复用和扩展,但滥用继承会导致类层次过深、耦合度过高,所以工程上更推荐组合优先于继承;多态则分编译时多态(重载)和运行时多态(重写),它让调用方依赖抽象而不是具体实现,这是开闭原则的基石。
这里我给一个加分答法:聊多态的时候顺手提一下"动态绑定"和"虚方法表",然后举一个项目里用接口解耦的例子。比如你有一个支付模块,定义了 PayService 接口,支付宝支付和微信支付各自实现它,订单服务只依赖接口。你不需要背定义,这段代码就是你对多态最好的理解。面试官听到这里基本就会把这一题从"过场题"升级为"加分题"。
2.2 equals、hashCode、重载与重写:高频追问链
这类题的可怕之处在于,它们天然形成一条越问越深的链条。第一问通常是"equals 和 == 有什么区别"。参考答案是:== 对基本类型比较的是值,对引用类型比较的是内存地址;equals 在类没有重写的时候等价于 ==,重写之后按业务规则比较内容,比如 String 重写后比较的是字符序列。
接着面试官会问"为什么要同时重写 equals 和 hashCode"。如果你的答案是"不重写 hashCode 就会出问题"这类话,等于没说。要讲清楚背后的逻辑:HashSet、HashMap 这类基于哈希的容器,会先用 hashCode 定位到桶,再用 equals 确认桶内是否有相同对象。如果两个对象 equals 相等但 hashCode 不同,它们会落在不同的桶里,导致同一个逻辑对象在集合中重复出现;如果 hashCode 相同但 equals 不等,则会在同一个桶里退化成链表或红黑树上的逐项比较。所以重写 equals 必须同步重写 hashCode,保证相等对象的哈希值一致,这是契约。
再往下可能追问 String 的 equals 和 StringBuilder 的 equals 为什么行为不同。String 重写了 equals 做内容比较,而 StringBuilder 没有重写,所以它继承的是 Object 的引用比较。这类问题没有标准死记的答案,你只要顺着"默认 equals 是引用比较、值比较需要重写"这条线推,就能答出来。
2.3 枚举、Lambda、数组越界:不起眼但常被翻车的点
"java枚举类型的使用"在热搜词里出现频率很高,因为它确实是个容易答得空泛的考点。枚举不仅仅是一组常量,它本身是类,可以有构造器、字段、方法,还能实现接口。最常见的实际用途是定义状态机和配置项。比如订单状态用枚举 PENDING(待支付)、PAID(已支付)、SHIPPED(已发货)、DONE(已完成),每个状态上挂一个描述和下一步允许的动作,比散落的魔法数字干净得多。面试里被问到枚举,建议顺带说一句"枚举在 switch 中可以作为 case 使用,并且天然是单例",这句话往往能引出"单例模式的最佳实现方式"这个话题。
Lambda 函数在 Java 8 之后是重点。考点通常不是语法本身,而是"Lambda 表达式和匿名内部类有什么区别"。核心答法是:Lambda 不生成独立的 class 文件,而是通过 invokedynamic 指令实现;它依赖函数式接口,比如 Runnable、Comparator;它对外部局部变量的捕获要求是 effectively final,也就是变量在捕获之后不能再被修改。能说出这三点,基本就过关了。
数组越界异常(ArrayIndexOutOfBoundsException)看起来太简单,但有人会把它和 StringIndexOutOfBoundsException 混淆,或者说不清它属于哪个异常体系。记住:它是 RuntimeException 的子孙,属于非受检异常,编译期不会强制要求处理,运行时才会暴露。答的时候如果能补一句"所以循环遍历时推荐用 i < length 而不是 i <= length - 1,边界判断要自己负责",说明你有真实写代码的敏感度。
3. 集合框架:HashMap 是绕不过去的一道坎
3.1 ArrayList vs LinkedList:别只停留在"数组和链表"的答案上
这道题的标准答案是"ArrayList 基于动态数组,查询快、插入慢;LinkedList 基于双向链表,插入快、查询慢"。但这只是第一层,面试官接着会问"ArrayList 插入一定慢吗"。答案是否定的——如果是在尾部追加,ArrayList 的均摊时间复杂度是 O(1),只有在触发扩容时才会有额外开销;而在头部插入,即使 LinkedList 也需要 O(n) 去找到对应节点,再加上非连续内存导致 CPU 缓存利用率下降,实际性能未必比 ArrayList 好。JDK 源码里 LinkedList 的注释都写着"使用索引进行随机访问的时间复杂度为 O(n)",所以在数据量大的场景下,LinkedList 的随机访问比 ArrayList 差得远。
另一个值得注意的点是内存占用。ArrayList 只需要维护一个数组的引用,而 LinkedList 的每个节点都要额外存前驱和后继指针。在存储大量小对象时,LinkedList 的内存开销几乎是 ArrayList 的两三倍。我自己的经验是:日常开发里 LinkedList 用得越来越少,多数列表场景 ArrayList 就够了,需要频繁头尾操作的场景优先考虑 ArrayDeque。面试时说出这一层,比单纯背答案要真实得多。
3.2 HashMap 的底层结构、扩容与树化机制
HashMap 是 Java 面试里当之无愧的题王。从 JDK 1.7 到 1.8,底层结构的变化、扩容逻辑、哈希算法、红黑树化条件,每一环都能出一个问题。准备这块的时候,我建议按下面这个顺序去理解。
第一,存储结构。JDK 1.8 之后是"数组 + 链表 + 红黑树"。put 一个键值对时,先根据 key 的 hashCode 做一次扰动运算(高 16 位异或低 16 位),然后与数组长度减一做按位与,定位到桶的位置。这里要答出"为什么用按位与而不是取模":因为数组长度是 2 的 n 次幂时,hash & (n - 1) 等价于 hash % n,但位运算效率更高。这个设计细节是 1.8 里散列更均匀的关键之一。
第二,冲突解决。同一桶内产生哈希冲突时,用链表存储;当链表长度超过 8 且数组长度达到 64 时,链表会转成红黑树,把查询时间复杂度从 O(n) 降到 O(log n)。为什么阈值偏偏是 8?源码注释里给了参考:在负载因子 0.75 的前提下,链表长度达到 8 的概率已经小到约千万分之六,所以这个阈值是空间和时间的权衡结果。能答出这条概率依据,面试官大概率会在这个点上给你打个高分。
第三,扩容机制。默认初始容量是 16,负载因子是 0.75,当元素个数超过容量乘以负载因子时,触发扩容到原来的两倍。扩容时元素要重新计算桶位。JDK 1.8 做了一个关键优化:元素在扩容后要么还在原位置,要么移动到原位置加旧容量的位置,判断依据是它对应的新增二进制位是 0 还是 1。这个优化省去了每次扩容都重新计算一遍 hash 的开销,是 1.8 对比 1.7 的重要改进。
3.3 fail-fast 与并发集合
集合这块另一个绕不开的问题是"ArrayList 在遍历时 remove 为什么会报 ConcurrentModificationException"。这对应的是一个叫 fail-fast 的机制:集合内部维护了一个 modCount 字段,每次结构性修改都会让它加一;迭代器在调用 next 时会检查 modCount 是否和创建迭代器时记录的值一致,不一致就立刻抛出异常。
这里有个很常见的追问:"那为什么用 Iterator 的 remove 就不会报错?"因为 Iterator.remove 在删除元素的同时,会同步更新 modCount 和 expectedModCount,让两个值保持一致。如果你在面试中还能补一句"fail-fast 并不能保证并发修改下一定抛异常,它只是一种快速失败的检测机制,不能替代同步手段",那就很稳了。
并发集合方面,ConcurrentHashMap 是必问项。JDK 1.8 版本放弃了分段锁,改用 CAS + synchronized 来保证并发安全。写操作时,如果桶为空就用 CAS 插入,如果不为空就用 synchronized 锁住桶的头节点。读操作通过 volatile 修饰的 Node 数组和节点的 next 指针来保证可见性。这个设计相比 Hashtable 的全表锁,锁粒度细了很多,并发性能大幅提升。答到这里如果能顺带对比一下 Collections.synchronizedMap 和 ConcurrentHashMap 的差异,面试官通常会给你一个正向反馈。
4. 并发与多线程:最耗脑力的主战场
4.1 synchronized 和 Lock:两条技术路线的差异
Java 多线程是很多人的软肋,因为它不像集合语法那样有标准代码可背,更考验对底层机制的理解。第一道高频题是"synchronized 和 Lock 有什么区别"。我给一个不漏要点的答法。
synchronized 是 JVM 层面的关键字,使用后由字节码指令 monitorenter / monitorexit 实现,锁的获取和释放在异常时由 JVM 自动完成,不需要手动管理。Lock 是 JDK 提供的接口,典型实现是 ReentrantLock,它需要手动 lock 和 unlock,通常要在 finally 块里释放锁。Lock 提供了 synchronized 没有的能力:可中断地获取锁(lockInterruptibly)、可超时获取锁(tryLock)、非阻塞地尝试获取锁,以及多个 Condition 条件队列。而 synchronized 在 JDK 1.6 之后引入了偏向锁、轻量级锁、重量级锁的升级过程,在竞争不激烈的场景下性能并不比 Lock 差。
这里有个加分的延伸是"公平锁与非公平锁":ReentrantLock 默认是非公平的,但可以通过构造函数传 true 变为公平锁;synchronized 本质上是非公平的,因为 JVM 的锁升级机制本身不保证线程按请求顺序获得锁。顺便说一句,非公平锁的实际吞吐量通常优于公平锁,因为它避免了频繁的线程切换开销,这也是它被设为默认值的原因。
4.2 volatile 到底保证了什么,不保证什么
volatile 是最容易被误解的关键字。很多人背的答案是"volatile 保证可见性和有序性,不保证原子性"。这句话没错,但面试官想听的是,你知不知道它为什么能做到可见性,又为什么做不到原子性。
可见性来源于 volatile 写操作前后的内存屏障。JVM 在写入一个 volatile 变量时,会在写操作前后插入 StoreStore 和 StoreLoad 屏障,强制把当前线程工作内存中的新值刷新到主内存;读取时插入 LoadLoad 和 LoadStore 屏障,保证读到的是主内存中的最新值。有序性来源于指令重排序限制:编译器不会把普通的写操作调到 volatile 写之后,也不会把普通的读操作调到 volatile 读之前。
为什么不保证原子性?因为像 count++ 这种操作,在字节码层面是"读-改-写"三步,volatile 只能保证每一步的可见性,但不能把这三步合并成一个原子操作。多个线程同时对 count 执行 count++,即使声明成 volatile,最终结果仍然会丢失更新。解决方案是加锁,或者用 AtomicInteger 的 CAS 操作。能把这段逻辑用"字节码层面三步"的方式讲出来,比单纯背"三个特性"要有说服力得多。
4.3 线程池:参数、执行流程与拒绝策略
"线程池的七个参数"是八股文里的标准题,但光背参数名没有意义。要真正理解,得从执行流程去看。
线程池提交一个任务时,首先判断核心线程数是否已满,没满就创建核心线程执行;满了之后任务进入阻塞队列等待;队列也满了,再判断最大线程数是否已满,没满就创建非核心线程执行;如果最大线程数也满了,就执行拒绝策略。这个流程对应着 ThreadPoolExecutor 构造器的七个参数:corePoolSize、maximumPoolSize、workQueue、keepAliveTime、unit、threadFactory、handler。
这里常见的追问是"队列选型有什么区别"。比如 LinkedBlockingQueue 和 SynchronousQueue:前者可以是有界或无界队列,队列很长的话可能永远不触发非核心线程的创建;后者不存储任务,直接交给线程执行,适合需要立即处理、不适合积压的场景。阿里开发规范里有个建议:手动创建线程池,明确指定有界队列和拒绝策略,避免使用 Executors 快捷方法带来的隐藏风险,比如无界队列可能导致内存无限增长。这个规范在面试里经常被拿来当引子,你能主动说出来,说明你关注线上风险。
拒绝策略有四种,它们的区别和适用场景可以整理成表格来看:
| 拒绝策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 直接抛出 RejectedExecutionException | 默认策略,适合不能接受任务丢失的场景 |
| CallerRunsPolicy | 由提交任务的调用线程执行 | 适合做天然限流,调用线程去执行任务时就不会再继续提交了 |
| DiscardPolicy | 静默丢弃任务 | 可以容忍任务丢失的场景,不推荐 |
| DiscardOldestPolicy | 丢弃队列中最旧的任务,再提交 | 追求最新任务优先的场景 |
我建议重点记 AbortPolicy 和 CallerRunsPolicy:前者是默认策略,后者在很多压测场景下能起到天然的背压效果,实际生产中很常用。
5. JVM 内存与类加载:OOM 排查能力的试金石
5.1 运行时数据区与对象的一生
JVM 的运行时数据区分为线程共享和线程私有两类。线程共享的是堆和方法区,在 JDK 1.8 中,方法区被移除,改成了元空间,使用的也不再是堆内存,而是本地直接内存;线程私有的是虚拟机栈、本地方法栈和程序计数器。
面试时常见的问法是:"一个 Java 对象的创建过程是什么?"完整答法是:类加载检查、分配内存、初始化零值、设置对象头、执行构造方法。分配内存时有指针碰撞和空闲列表两种方式,取决于堆内存是否规整,而是否规整又取决于所用的垃圾收集器是标记-复制还是标记-清除。对象头里存了 Mark Word 和类型指针,Mark Word 里记录了哈希码、GC 分代年龄、锁状态标志等信息。这些细节每一条都可以作为追问点,你在准备时最好把它们串成一个连贯的故事,而不是零散地背几个名词。
5.2 类加载机制与双亲委派模型
类加载机制是 JVM 板块的另一半。类的生命周期包括加载、验证、准备、解析、初始化、使用、卸载七个阶段。面试常问的是"什么是双亲委派模型"。
答法是:当一个类加载器收到加载请求时,它不会自己先尝试加载,而是把请求委派给父类加载器,逐级向上,最终由启动类加载器尝试加载;只有当父加载器加载失败时,子加载器才会自己去加载。双亲委派的好处是避免核心类被重复加载和篡改。比如你在自己的代码里写了一个完全同名的类,最终还是由启动类加载器加载 JDK 自带的版本,保证了类型体系的安全。
这里有个加分细节:很多框架会通过"打破双亲委派"来实现特殊功能,比如 Tomcat 的 WebAppClassLoader。它为了隔离不同 Web 应用的类,会自己先尝试加载,而不是先委派给父加载器。能举出这个反例,说明你不只是背了概念,而是真的理解这个机制存在的意义和它的边界。
5.3 从 OutOfMemoryError 到排查思路
热搜词里有一条"java: outofmemoryerror: insufficient memory",看来这个报错折磨了不少人。实际上 OutOfMemoryError 是个大家族,需要区分不同的根因:
- 堆内存溢出(Java heap space):多半是对象太多或存在内存泄漏。常见于大集合无上限增长、缓存不清理、一条 SQL 查出超大结果集。
- 元空间溢出(Metaspace):常见于动态生成类过多的场景,比如大量使用 CGLIB 代理、频繁热部署。
- 栈溢出(StackOverflowError):通常由无限递归或过深的调用链导致。
- 本地内存不足(Insufficient memory):这类报错通常不是 Java 堆空间不够,而是操作系统或进程可用的直接内存不足,和启动参数里堆设置过大、线程数过多、加载了太多本地库都有关系。
遇到 OOM,我的建议是按这个顺序排查:先看日志确认是哪个区域溢出;然后通过 jmap 或 MAT 分析堆转储文件,找到占用内存的大对象和引用链;接着检查代码里是否有缓存无上限、流未关闭、ThreadLocal 滥用、静态集合持有业务对象这类典型问题;最后压测复现,确认修复效果。能在面试里把这个排查链路完整说出来,比回答任何一个单一知识点都加分,因为这代表你有处理线上问题的实战能力。
6. 手撕代码和工程化细节:最后一块照妖镜
6.1 冒泡排序和快速排序怎么写才显得"有水平"
热搜词里"冒泡排序java"和"快速排序java实现"的热度一直很高,说明手撕排序算法仍然是很多公司笔试和面试的固定环节。冒泡排序本身不难,但很多人写出来的代码有个常见毛病:缺少提前退出机制。加上 swapped 标记之后,对一个已经有序的数组,第一轮遍历就能发现没有发生交换,直接结束,时间复杂度从 O(n²) 降到 O(n)。这个优化很小,但在面试官眼里代表你考虑过"是否真的需要继续遍历"。
快速排序的写法值得多说几句。教科书式的写法是选一个基准值,通过挖坑法或左右指针交换法把数组分成两部分,然后递归排序。写的时候有几个容易出错的点:递归终止条件要用 left >= right,如果只写 left == right,某些情况下会陷入无限递归,最终栈溢出;分区时两个指针的移动要有边界判断,防止越界;基准值如果选得不好,比如对近似有序的数组总是选到最小或最大值,就会退化成 O(n²)。面试时如果你能主动说"可以用三数取中法或者随机选基准来降低退化概率",证明你真的理解这个算法的本质,而不是背了一段代码。
6.2 环境配置类问题的真实考点:JDK、环境变量、Lombok
很多新人面试前不好好准备环境配置问题,结果一上来就被问懵了。比如"java环境变量配置详细教程"这类热搜背后,其实藏着两个面试官真正关心的点:你懂不懂 PATH 和 CLASSPATH 的区别,以及遇到环境异常时你会不会自己解决。
PATH 是操作系统用来查找可执行文件(java、javac)的路径;CLASSPATH 是 JVM 用来查找类库的路径,JDK 9 引入模块化之后加载方式有了一些变化,但基本概念没变。面试常见的场景题是:"编译时报错 'java: 警告: 源发行版 17 需要目标发行版 17',怎么解决?"原因就是项目设置的 Java 版本和当前 JDK 版本不一致,通常去 IDE 里调整 Project Structure 和编译器版本就能解决,或者检查构建工具里编译插件的 source/target 配置。
Lombok 相关的报错也很常见,比如"java: you aren't using a compiler supported by lombok, so lombok will not work"。这通常是因为 JDK 升级之后,项目里用的 Lombok 版本太旧,不兼容新的编译器。解决办法是升级 Lombok 依赖到对应版本的库,同时更新编译器的注解处理器配置。这类问题在面试里出现时,面试官不是考你背不背得下报错信息,而是看你有没有自己动手排查过环境问题的经验。
6.3 框架和工程类考点:Spring Boot 接口安全、测试框架、日志
常规 Java 面试到了框架轮,Spring Boot 是绕不开的。几个常见考点先说清楚。
第一个是接口安全。经常有候选人被问到"Spring Boot 接口怎么防止别人乱调用"。合理的答法至少应该包括:接口鉴权(Token / JWT,或者 AppKey + 签名)、请求参数校验(Spring Validation)、防重放(时间戳 + nonce)、限流(Guava RateLimiter 或 Redis + Lua 脚本)、HTTPS 传输加密、敏感字段脱敏。关键不是把所有方案列一遍,而是告诉面试官在项目里怎么组合使用,比如"签名机制里带了时间戳和随机串,用来防止重放攻击"。
第二个是测试框架。搜"java接口自动化测试框架"的人很多,说明这已经不只是测试岗位的需求,开发岗也开始强调可测试性。Spring Boot 项目里常用的方案是 Spring Boot Test + MockMvc + AssertJ,接口级别的测试可以用 RestAssured,需要真实环境时可以用 Testcontainers 起容器。面试时能说出"我写过多少个单元测试、怎么 mock 外部依赖、怎么做接口回归",比只说我用过 JUnit 要有说服力得多。
第三个是日志和异常处理。统一异常处理用 @ControllerAdvice + @ExceptionHandler,日志用 SLF4J + Logback,通过 MDC 追踪请求链路,这些难度不大,但很能反映一个人的工程素养,因为日常开发里几乎天天遇到。
说实话,每次看到网上有人求一份"java八股文合集",我都会想起自己第一次面试时对着 HashMap 原理卡壳的场景。那一场我挂了,但那次挂让我明白了一个道理:八股文从来不是考点本身,它只是一个索引,索引背后是整张 Java 知识网。后来我自己坐在面试官那一侧,看到候选人能把 synchronized 和 Lock 的差别、volatile 的内存屏障、线程池的拒绝策略串成一个完整的故事,我就知道这个人不是背题背出来的,是真的写过代码、读过源码、踩过坑的人。如果你正在准备面试,我不建议你对着一份动辄几百道的题集硬啃。找一个下午,从 HashMap 开始,自己给自己从头讲一遍,讲到讲不动为止,那才是八股文真正该有的复习方式。