简介:面向Java面试者的高频考点梳理文档,围绕Spring Bean生命周期、Spring MVC执行流程、Spring Boot自动装配原理、SQL优化与MySQL索引等核心模块展开,内容紧凑、要点明确,适合准备校招、社招或复习框架与数据库知识的Java开发人员快速查阅。PDF共1个文件,压缩包大小约396KB,便于在手机、电脑和平板上随时浏览。目前已有180人学习下载。文档对每个知识点都做了分层归纳:例如Bean生命周期按实例化、依赖注入、初始化、销毁四个阶段解析,Spring MVC流程从请求发起到视图响应逐步说明,同时补充了执行计划字段解读、索引失效场景、常见索引类型等内容,并给出明确的优化建议。读者可通过这份资料快速建立面试答题框架,查漏补缺,重点突破Spring与SQL这两大类高频考题。
1. Java 高频面试题 PDF:一份能直接背进脑子里的考点清单
准备 Java 后端开发面试的人,多半经历过这种状态:面经收藏了几十篇,知识点零散地躺在笔记里,真要开口回答时却组织不成话。这份《Java 高频面试题.pdf》的价值,恰恰在于它把 Spring、MySQL、并发编程、集合框架、Redis 这几大块的常考题目按索引方式整理好了,每条都直接给出可背诵的结论和关键词。它不是什么源码剖析,也不是完整的项目实战,而是一份「考点地图」:适合校招冲刺、社招复习、以及想系统性查漏补缺的开发者。核心思路很朴素:先把答案骨架记住,再往骨架里填理解,比从零啃源码要快得多。
2. Spring 三件套:Bean 生命周期、MVC 执行流程与自动装配的答题主线
Spring 相关题目在 Java 面试里占比极高,而这份 PDF 里最值钱的内容,就是它把三个最容易被追问的机制拆成了可以背的步骤。常见的失败方式是概念都知道,但一深问就乱。这一章我按面试官追问的顺序,把三块内容的记忆锚点过一遍。
2.1 Bean 生命周期:实例化、初始化、销毁的顺序怎么答
Bean 生命周期是 Spring 面试的必问题。PDF 给出的主线很简洁:实例化(实例反射或工厂方法创建)→ 依赖注入(属性值、对象依赖)→ 初始化(实现 InitializingBean 接口的 afterPropertiesSet(),或配置 init-method)→ 销毁(实现 DisposableBean 的 destroy(),或配置 destroy-method)。
面试时只背这条线是不够的,因为面试官大概率会追问「容器到底怎么把 Bean 造出来的」。这里我一般会补一层:实例化用的是反射创建对象,此时对象还是半成品;依赖注入发生在实例化之后,通过 setter 或字段注入把依赖填进去;初始化阶段才调用 InitializingBean 或自定义 init 方法。代码里最常见的是这种写法:
public class UserService implements InitializingBean, DisposableBean { private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public void afterPropertiesSet() throws Exception { // 依赖注入完成后,可以在这里做初始化校验 // 比如检查 userDao 是否为 null,预热缓存等 System.out.println("UserService 初始化完成"); } @Override public void destroy() throws Exception { // 容器关闭时释放资源 System.out.println("UserService 销毁"); } }逻辑说明:afterPropertiesSet() 是 InitializingBean 接口定义的方法,容器在 Bean 属性填充完成后自动调用,适合做依赖完整性校验或资源初始化。destroy() 则是 DisposableBean 接口的方法,容器关闭时回调,用于释放连接、关闭线程池这类收尾工作。参数说明:如果一个 Bean 同时配置了 init-method 和 InitializingBean,Spring 的执行顺序是先调用 afterPropertiesSet(),再调用自定义的 init 方法,这个顺序面试里经常被当作陷阱题。
再往后,如果面试官想压深度,会问 BeanPostProcessor 在哪个阶段介入。PDF 里没展开,但你可以记住这句话:BeanPostProcessor 的 postProcessBeforeInitialization 在初始化方法调用之前执行,postProcessAfterInitialization 在初始化之后执行——AOP 代理就是靠后置处理器在初始化之后生成的。答到这一层,基本就能区分开「背过答案」和「真理解」的候选人了。
2.2 Spring MVC:八步流程的前端控制器主线
Spring MVC 的执行流程,PDF 给了一条完整链路:客户端发 HTTP 请求 → DispatcherServlet 前端控制器接收 → HandlerMapping 把 URL 映射到处理器方法 → HandlerAdapter 调用控制器方法 → Controller 执行业务逻辑返回模型 → ViewResolver 解析视图 → 渲染 HTML → 响应客户端。
这条链路里,面试官最看重的是你能不能抓住 DispatcherServlet 的核心地位。整条链路除了「控制器处理请求」「视图渲染」「响应客户端」这三个环节之外,其余环节都要把控制权交回给前端控制器。换句话说,DispatcherServlet 是总调度,其他组件都是被它调用的零件。
这里有一个值得记的答法:把八个步骤压缩成四段话。第一段,请求进来先过前端控制器;第二段,前端控制器问 HandlerMapping 要「这个 URL 该交给谁处理」,拿到 Handler 后让 HandlerAdapter 去调用真正的 Controller;第三段,Controller 返回 ModelAndView,前端控制器再把视图名交给 ViewResolver,解析出真正的页面;第四段,页面渲染完成后,由前端控制器把响应写回客户端。四段话对应四个核心组件,面试官再追问细节,你再往八步里展开。
容易翻车的点是 HandlerAdapter 的作用。很多人会跳过它,直接从 HandlerMapping 跳到 Controller。实际上 HandlerAdapter 是适配器模式的典型应用:它负责把不同类型的 Handler(比如 @RequestMapping 注解方法、HttpRequestHandler、Controller 接口实现类)统一适配成可执行的调用,Controller 方法的参数绑定、返回值解析也都是在这里完成的。回答流程题时把它带上,会显得你对框架的抽象层次有概念。
2.3 Spring Boot 自动装配:从 @SpringBootApplication 到条件注解
Spring Boot 的自动装配原理,核心就在 @SpringBootApplication 这个组合注解上。PDF 拆出的三个组成注解分别是 @SpringBootConfiguration、@ComponentScan、@EnableAutoConfiguration。前两个好理解:一个标识配置类,一个指定扫描包路径。真正有含金量的是 @EnableAutoConfiguration。
它的底层是通过 @Import 注解导入 AutoConfigurationImportSelector 实现类,这个类会调用 getAutoConfigurationEntry() 方法,读取并筛选自动配置候选类列表。筛选时会看类路径下有没有对应的依赖 jar,再结合 @Conditional 系列条件注解判断要不要真正生效。
为了把这个抽象过程落地,我自己做自定义 starter 时会写一个这样的配置类:
// 文件路径:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports // com.example.demo.autoconfigure.DemoAutoConfiguration @AutoConfiguration @ConditionalOnClass(DemoService.class) public class DemoAutoConfiguration { @Bean @ConditionalOnMissingBean public DemoService demoService() { return new DemoService(); } }逻辑说明:@AutoConfiguration 标记这是一个自动配置类;@ConditionalOnClass 表示类路径下存在 DemoService 时才加载这个配置;@ConditionalOnMissingBean 表示容器里还没有 DemoService 实例时才创建默认 Bean。参数说明:AutoConfiguration.imports 是 Spring Boot 2.7 之后的配置文件路径,老版本常用 META-INF/spring.factories。面试里问到自动装配的底层原理时,把这个机制讲清楚就够用了。
另外面试时建议补一句:自动装配解决的是「依赖存在但没显式声明 Bean」的问题,而不是取代手动配置。你依然可以通过 exclude 属性排除掉不需要的自动配置类别,或者靠 @ConditionalOnMissingBean 让自定义 Bean 优先生效。
3. MySQL 优化:Explain 执行计划、索引失效与 B+Tree 的排查思路
MySQL 这块是 Java 后端面试的另一座大山。PDF 把内容分成了执行计划、索引类型、索引失效、B+Tree 底层四部分,这是很标准的「理论与排查结合」的组合。这一章我按实际排查 SQL 的思路来讲,而不是按 PDF 的目录顺序复述。
3.1 Explain 怎么看:从 type、key 到 Extra 的排查顺序
拿到一条慢 SQL,第一步永远是加 explain 关键字看执行计划。命令格式很简单:
EXPLAIN SELECT u.id, u.name, o.order_no FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id WHERE u.status = 1 AND u.create_time > '2024-01-01';逻辑说明:explain 不会真正执行这条 SQL,它只是让 MySQL 输出优化器预估的执行方案。参数说明:重点看 type、key、rows 和 Extra 这四个字段。type 是访问类型,性能从好到坏大致是 system > const > eq_ref > ref > range > index > ALL。PDF 里提到的 system 和 const 是理想情况——根据主键或唯一索引精确匹配到一行;ref 是非唯一索引等值匹配;range 是索引范围扫描;index 是扫描整个索引树;ALL 就是全表扫描,这是最需要警惕的。
key 字段表示实际用到的索引,为 null 就是没走索引,这时候优先检查 where 条件里的列有没有建索引、有没有触发索引失效。rows 是优化器预估要扫描的行数,数值越大越危险。Extra 里常见几个值得注意的值:Using filesort 说明排序没走索引,需要额外排序;Using temporary 说明用了临时表,多出现在 group by 或 union 的场景;Using index 是好事,说明覆盖索引直接返回结果,不需要回表。
我在实际排查时会按这个顺序:先看 type 是否糟糕,再看 key 是否为 null,然后看 rows 是否过大,最后看 Extra 里有没有 using filesort 或 using temporary。把这四个字段看完,一条 SQL 的主要问题基本就定位了。
3.2 索引失效场景与 B+Tree 的取舍
PDF 里列的索引失效场景很全:使用函数或表达式、使用 NOT 或 !=、or 操作符、like 前置百分号、多个索引只有第一个有效。实际开发里还有两个高频场景容易漏。一个是隐式类型转换,最常见的是 varchar 字段直接用数字去查,MySQL 会在底层把字段转成数字比较,导致索引失效,例如:
-- phone 字段是 varchar 类型,但用数字常量去查,会触发隐式类型转换 SELECT * FROM t_user WHERE phone = 13800000000;另一个是违反最左前缀原则。PDF 提到组合索引只有第一个有效,准确说法应该是:组合索引 (a, b, c) 中,查询条件必须从 a 开始才会走索引;跳过 a 直接查 b 或 c,索引就用不上。这是组合索引的核心约束,面试常考。
B+Tree 这块,PDF 给的计算逻辑很值得记住:非叶子节点不存数据,只存索引键和指针,每个节点约 16KB,一个 key 约 8 字节,一个指针约 6 字节,所以一个非叶子节点能存约 1000 条索引项,第二层能索引到约 100 万条数据,第三层就覆盖千万级数据。因此 InnoDB 的索引树通常只有 2 到 4 层,查询从根节点到叶子节点只需要几次磁盘 I/O,这是 B+Tree 能支撑大数据量查询的根本原因。
对比一下其他索引类型:PDF 还提到 Hash 索引、R-Tree 空间索引、Full-text 全文索引。Hash 索引等值查询极快但不支持范围查询,R-Tree 用在地理坐标数据,Full-text 用于全文检索。日常业务表里默认的 B-Tree 索引(准确说是 B+Tree)依然是最通用的选择,因为它同时支持等值、范围、排序。
3.3 SQL 优化落地:从 select * 到 limit 1 的常见做法
优化规则 PDF 写了不少,但面试时你要能解释「为什么」。比如「select 时指明字段,避免 select *」,原因是减少回表次数和网络传输量;如果查询字段正好被覆盖索引包含,还能直接走 Using index 连回表都省了。再比如「分页只有一页时用 limit 1」,意义在于让优化器知道只查一行,找到后立即停止扫描。
几个我常用的落地习惯,供参考:能用 inner join 就不要用 left join,因为 inner join 会让优化器有更多表顺序选择空间;排序字段没建索引就尽量避免排序,否则会有 using filesort;where 条件里避免 null 判断,因为 null 判断通常导致索引失效;like 模糊查询如果必须用前置百分号,可以考虑用覆盖索引兜底或改成全文检索;用 union all 代替 union,因为 union 会去重,自带临时表开销。
这部分的答案不应该只背「in 的取值不宜过多」这种结论,还要能说出「为什么」。in 值过多时,优化器可能放弃索引走全表扫描,或者生成大量等值比较条件,导致执行计划不稳定。把它理解为「控制查询复杂度」而不是「禁止 in」,才是正确的姿势。
4. 并发编程:线程池参数、线程状态与死锁的实战边界
并发是区分候选人层次的板块。PDF 覆盖了线程池种类、核心参数、执行流程、线程状态、三大特性、死锁。这一章的重点是:参数怎么设、状态怎么流转、以及死锁在你的代码里会怎么出现。
4.1 线程池:七个核心参数与执行流程
PDF 给出了 Executors 提供的四种快捷线程池,但面试官考察的重点往往是 ThreadPoolExecutor 的核心构造参数。我实际项目里基本不用 Executors,因为 fixed 和 single 线程池的队列是无界的,任务堆积时会撑爆内存。更常用的做法是手动构造:
ThreadPoolExecutor executor = new ThreadPoolExecutor( 4, // corePoolSize 核心线程数 16, // maximumPoolSize 最大线程数 30L, // keepAliveTime 救急线程空闲存活时间 TimeUnit.SECONDS, // unit 时间单位 new ArrayBlockingQueue<>(100), // workQueue 等待队列 r -> new Thread(r, "biz-pool-" + r.hashCode()), // threadFactory new ThreadPoolExecutor.CallerRunsPolicy() // handler 拒绝策略 );逻辑说明:任务提交后,先判断核心线程是否满;没满就创建核心线程执行,满了就把任务放入 workQueue;队列也满了,才创建救急线程(数量上限是 maximumPoolSize - corePoolSize);救急线程也满且队列满,触发拒绝策略。参数说明:corePoolSize 是常驻线程数,建议根据 CPU 密集还是 I/O 密集来估,I/O 密集场景可以卡在「IO 等待时间占比」附近算;maximumPoolSize 要结合系统资源上限设置,别盲目扩;workQueue 用有界队列比无界队列安全得多。
关于拒绝策略,PDF 列了四种:AbortPolicy 直接抛异常,是默认策略;CallerRunsPolicy 让提交任务的线程自己去跑,相当于变相降低提交速度;DiscardOldestPolicy 丢弃队列里最早的任务;DiscardPolicy 直接丢弃当前任务。实际中最推荐 CallerRunsPolicy,它既能限流又不会静默丢数据,缺点是需要留意调用线程被阻塞的情况。
4.2 多线程三特性与 volatile、synchronized 的边界
PDF 把原子性、可见性、有序性讲得很清楚:原子性是操作不可分割,可见性是线程修改后其他线程能读到最新值,有序性是防止指令重排带来的意外结果。面试时容易被追问的是「volatile 到底能保证什么」。它能保证可见性和有序性,但保证不了复合操作的原子性。经典的错误示范是用 volatile 修饰计数器:
private volatile int count = 0; public void increase() { count++; // 这不是原子操作,volatile 保证不了 }逻辑说明:count++ 实际上是「读取 → 加一 → 写回」三步,两个线程同时读到旧值再写回,就会丢更新。参数说明:volatile 适合「一个线程写、多个线程读」的开关变量,比如状态标志;需要原子性的场景必须用 synchronized、Lock 或者 AtomicInteger。synchronized 可以同时保证原子性、可见性、有序性,但粒度粗;Lock 提供了更细的控制,比如 tryLock 带超时,这是避免死锁的重要手段之一。
4.3 线程状态流转与优雅退出
线程状态这条线,PDF 列出了七种状态:新建、可运行、运行中、阻塞、等待、超时等待、终止。面试最常挖的坑是「阻塞和等待的区别」。按我的理解,阻塞是线程想拿到一个锁但没拿到,或者进入了 synchronized 代码块在等 monitor 锁;等待是因为调用了 Object.wait()、join()、LockSupport.park() 这类方法,需要其他线程显式唤醒。阻塞是不需要别人唤醒的,锁一释放就继续;等待必须有人 notify 或 unpark。
优雅退出线程在真实项目里比想象中高频。不要用 Thread.stop(),那是强杀。我一般用标志位配合 interrupt:
public class Worker implements Runnable { private volatile boolean running = true; @Override public void run() { while (running && !Thread.currentThread().isInterrupted()) { // 执行任务,每次循环检查一次退出条件 doWork(); } // 清理资源 release(); } public void shutdown() { running = false; Thread.currentThread().interrupt(); } }逻辑说明:volatile running 保证多线程间的可见性,isInterrupted() 捕获外部中断请求,两者都满足才继续循环。参数说明:shutdown() 里先置 false 再 interrupt,能同时覆盖「正在等待任务」和「正在执行任务」两种状态,比单用标志位更可靠。面试里提到 join() 也可以顺带说一句:它让主线程等子线程结束,本质也是阻塞当前线程直到目标线程终止。
5. 高频坑位排查:事务失效、循环依赖与 HashMap 扩容的三个重灾区
这一章集中处理这份 PDF 里最容易「背了还是会踩」的三组问题。每一组我都按「现象 → 原因 → 解决」的结构来拆,这也是面试回答情景题最稳妥的框架。
5.1 事务失效:现象、原因与解决
现象一:方法标了 @Transactional 却没回滚。原因:异常被 try-catch 吞掉了,或者抛出的是非运行时异常。Spring 默认只在 RuntimeException 和 Error 时回滚,受检异常默认不回滚。解决:要么让异常继续向上抛,要么在注解上明确 rollbackFor = Exception.class。
现象二:同一个类内部调用另一个 @Transactional 方法,事务不生效。原因:Spring 事务基于 AOP 代理实现,自调用走的是 this 方法,不会经过代理对象,事务切面根本没进来。解决:把事务方法拆到另一个 Bean 里,或者自注入代理,更常规的做法是直接用 TransactionTemplate 做编程式事务。
现象三:静态方法、非 public 方法、final 方法上加 @Transactional。原因:代理机制对静态方法和 final 方法无能为力,非 public 方法在某些代理模式上也有裸露面。解决:事务方法必须是 public 且不能被 final 修饰。
现象四:数据表本身不支持事务,比如 MyISAM 引擎。原因:DML 操作根本没有事务能力。解决:换 InnoDB。这四条是事务失效最高频的成因,面试官喜欢从「方法内部调用」这个场景往下挖,必须能脱口而出代理机制这个根因。
5.2 循环依赖:三级缓存能救什么,救不了什么
循环依赖的场景:两个 Bean 相互持有对方,比如 A 依赖 B,B 又依赖 A。PDF 里有一个值得注意的前后矛盾之处:前面写「基于构造注入循环依赖 spring 可以自动处理」,后面又写「基于构造注入循环依赖 spring 无法自动处理」。以目前主流的 Spring 版本来说,构造注入的循环依赖是无法被三级缓存解决的,因为构造 Bean 时必须先有完整的构造参数,此时 A 还没创建完,B 拿不到 A 的实例。字段注入和 setter 注入可以使用三级缓存,核心流程是:创建 A 的早期引用放入三级缓存 -> B 依赖 A 时从缓存拿到早期引用 -> 完成 B -> 再回来完成 A。
三级缓存的职责要分清:第一层 singletonObjects 存放完整单例 Bean;第二层 earlySingletonObjects 存放早期暴露的半成品 Bean;第三层 singletonFactories 存放能生成早期引用的 ObjectFactory。解决循环依赖有两条路,一是依赖注入方式从构造注入改成字段注入或 setter 注入,让三级缓存有发挥空间;二是加 @Lazy 延迟其中一个依赖的初始化,打破实例化时的依赖闭环。面试里建议主动说出「三级缓存解决的是单例和非构造注入场景,构造注入依赖必须靠 @Lazy 或重构」这个边界,信息量判断力是满分的关键。
5.3 HashMap:1.7 死循环到 1.8 红黑树的演进
HashMap 的存储结构,PDF 写得很明确:JDK 1.7 及以前是数组加链表,头插法;JDK 1.8 之后是数组加链表加红黑树,尾插法。1.7 的并发扩容死循环问题是经典面试题:多线程同时扩容时,头插法会让链表倒置,形成环,之后 get 操作就陷入死循环。1.8 改用尾插法,解决了环的形成问题,但 HashMap 依然不是线程安全的,并发场景要用 ConcurrentHashMap。
面试进阶版会问:为什么 1.8 还要引入红黑树?答案是当链表长度超过阈值 8、且数组长度大于等于 64 时,链表转树,把最坏查找复杂度从 O(n) 降到 O(log n),削弱 hashCode 分布不均匀时的性能衰减。但树化不是免费的,红黑树节点占内存更大,所以树退化回链表的阈值是 6,留了缓冲区避免频繁转换。
还有一个高频考点是「HashMap 的默认负载因子是 0.75,为什么不是 1 也不是 0.5」。0.5 浪费空间,1 的含义是数组快满才扩容,但此时链表会很长,哈希冲突严重。0.75 是空间和时间的折中。以及 key 可以为 null,但只允许一个 null 键,null 键被哈希到数组下标 0 位置。把 HashMap 的面试题串成这样,「为什么」的层数越挖越深,比只背结论稳得多。
6. 最后的临考技巧:用 60 分钟自测把 PDF 考点变成肌肉记忆
拿到 PDF 之后,最容易犯的错是当成小说从头读到尾,读的时候觉得都会,面试时张口却组织不出完整答案。我从某次翻车经历里总结出一个习惯:把 PDF 的每一章转成自测题卡,然后用一小时做一次「三遍输出」式自测。
第一遍是复述:打开一节内容,遮住回答部分,只对着题目用自己的话讲一遍,卡住的地方就是薄弱点。第二遍是写要点:把刚才能讲的内容压缩成 5 到 10 个关键词,比如 Bean 生命周期就是「实例化、注入、InitializingBean、init、DisposableBean、destroy」,做到看一眼关键词就能展开整段话。第三遍是追问模拟:针对每个考点预设面试官可能的追问方向,比如讲了事务失效,就要立刻条件反射讲出「代理机制」「rollbackFor」「自调用」这几个关键点。
我把 PDF 的内容映射成一个 60 分钟自测表,供参考:
| 时间段 | 自测范围 | 自测方式 |
|---|---|---|
| 0-15 分钟 | Spring Bean 生命周期、MVC 流程、自动装配 | 手绘流程链,口述每一步组件职责 |
| 15-30 分钟 | MySQL 执行计划、索引失效、B+Tree | 随机写三条失效 SQL,说出原因和修复 |
| 30-45 分钟 | 线程池参数、三特性、线程状态 | 现场构造一个 ThreadPoolExecutor,说明每个参数 |
| 45-55 分钟 | 事务失效、循环依赖、HashMap 演进 | 举一个事务失效场景,按现象-原因-解决回答 |
| 55-60 分钟 | 回顾薄弱点 | 把卡壳的知识点再复述一遍 |
这套自测方法不必等到临考前才用。我把 PDF 里每章的核心题目都抽成卡片,碎片时间就拿卡片自问自答,答不上的回去翻 PDF 对应段落,比反复全文通读有效得多。从那以后,我每次准备面试都会强制自己先跑一遍这个流程,把考点变成条件反射。希望帮到你。
本文还有配套的精品资源,点击获取