每年到了春招秋招的节点,总能看到一批人拿着“Java后端笔试复习资料”猛刷,刷完自我感觉良好,真上考场却发现题目问法和自己想的完全不一样。我做了这么多年后端开发,也帮公司面过不少人,一个很深的感受是:Java后端笔试其实并不是考你背了多少API,而是考你对这门语言底层机制的理解深度,以及面对一个实际场景时能不能做出合理判断。
这篇内容是“Java后端笔试知识点复习”系列的第一篇,我想把后端笔试里出现频率最高、也最容易丢分的几块内容——Java基础、集合框架、并发编程、JVM和Spring核心——用一线开发的实际视角重新捋一遍。不是给你罗列面经题,而是把每个考点背后的“为什么”讲清楚。适合正在准备后端岗位笔试和面试的同学,也适合工作一两年想回头补基础的朋友。看完这篇,你至少能对笔试的考察逻辑有个整体把握,知道该往哪个方向使劲。
1. Java基础考点:不是背定义,是看理解深度
Java基础这部分,笔试里看着简单,实际是拉开差距的地方。很多题表面上考语法,实际考的是你踩没踩过坑。我给公司出笔试题时,最喜欢在基础题里埋一些平时写代码很少注意的细节,这部分能刷掉一大批“背了八股但没写过代码”的候选人。
1.1 数据类型与自动装箱的坑
Java的数据类型分基本类型和引用类型,笔试常考两者的区别、默认值、存储位置,这些是送分题。真正容易出错的是自动装箱相关的题目。看下面这段,几乎每次笔试都能见到变体:
Integer a = 127; Integer b = 127; Integer c = 128; Integer d = 128; System.out.println(a == b); // true System.out.println(c == d); // false第一次看到这个结果的人基本都会懵。原因在于Integer内部有一个缓存类,默认缓存了-128到127之间的对象。用Integer a = 127这种写法时,JVM会调用valueOf方法,在这个范围内直接返回缓存对象,所以a和b指向同一个对象,==比较的是引用地址,自然相等。而128超出缓存范围,每次都会新创建一个对象,c和d指向不同对象,比较结果就是false。
这个知识点背后真正想考的是你对“==比较引用、equals比较值”这个底层逻辑是否清楚。我在实际开发中确实遇到过有人因为用==比较Integer导致线上bug的,所以笔试考这个完全不是无的放矢。复习时建议顺便把Long、Short、Character、Boolean的缓存范围也过一遍,其中Boolean只有true和false两个缓存对象,这个笔试偶尔也会考。
1.2 面向对象三大特性在笔试中的问法
封装、继承、多态,网上资料一搜一大把,但我发现很多人只记住了一个名字,真用代码验证就露馅。笔试最典型的考法是给你一段继承代码,问你输出顺序。比如这样:
class Parent { static { System.out.print("Parent static "); } { System.out.print("Parent instance "); } Parent() { System.out.print("Parent constructor "); } } class Child extends Parent { static { System.out.print("Child static "); } { System.out.print("Child instance "); } Child() { System.out.print("Child constructor "); } } // 执行 new Child() 后输出什么?正确的加载顺序应该是:父类静态代码块、子类静态代码块、父类实例代码块、父类构造方法、子类实例代码块、子类构造方法。也就是说静态部分先执行且只执行一次,然后按“先父后子”的顺序执行实例初始化和构造。这个知识点其实考察的是JVM加载类和创建对象的完整流程,笔试爱考的原因就在这里——它能把“类加载”和“对象创建”两个底层概念串在一起。
多态部分笔试最常见的是“重载和重写的区别”,以及“编译期类型与运行期类型不一致时调哪个方法”。记住一个核心原则:方法调用看运行期实际类型,变量属性访问看编译期声明类型。比如Parent p = new Child(); p.someMethod()到底调用谁的实现,取决于someMethod是不是被Child重写了,如果重写了就调用Child的版本,这就是动态绑定。这个坑我在代码评审里见过很多次,有人以为属性也会有动态绑定效果,结果到处踩雷。
1.3 字符串与异常处理的重点
字符串几乎是Java笔试的必考区,核心就是String、StringBuilder、StringBuffer三者的区别,以及字符串常量池的机制。比如:
String s1 = "abc"; String s2 = new String("abc"); System.out.println(s1 == s2); // false System.out.println(s1.equals(s2)); // true"abc"这种字面量会直接放到字符串常量池,而new String("abc")会在堆上创建一个新对象。常量池里已经有"abc"的情况下,new出来的对象不会复用常量池里的引用,所以==的结果是false。String不可变、适合做HashMap的key、适合多线程共享,但字符串拼接不要用+,尤其循环里,性能差得离谱,应该用StringBuilder。StringBuffer加了同步,线程安全但性能略低,单线程场景基本不会选它。
异常部分常考的是受检异常和非受检异常的区别,以及try-catch-finally中finally和return的执行顺序。finally块一定会执行,但它里面如果有return,会覆盖try或catch里的return值,这是一个反直觉的坑。还有System.exit()会终止JVM,此时finally不会执行,这个细节笔试偶尔会作为陷阱题出现。
2. 集合框架:后端笔试的“主战场”
如果说Java基础是开胃菜,那集合框架就是笔试的硬菜。几乎每家公司的笔试题里都有集合相关的内容,尤其是HashMap。不是因为这东西多高级,而是后端开发每天都要跟数据存储和查询打交道,集合用得好不好直接反映代码水平。这部分我建议你抛开“会用”的层面,深入到“底层怎么实现”去看。
2.1 HashMap的底层结构、扩容与并发问题
HashMap的底层结构是“数组 + 链表 + 红黑树”,这个可能人人都知道,但笔试真正会考的是更细的点。比如为什么要用红黑树?因为当哈希冲突严重时,链表查询复杂度退化成O(n),数据量大到一定程度就扛不住了,红黑树可以把查询控制在O(log n)。那为什么不一开始就用红黑树?因为树节点占用的内存是普通节点的两倍左右,数据量小的时候反而更浪费,所以JDK设置了一个阈值——链表长度到8且数组长度到64时转为红黑树。
扩容机制也是个高频考点:默认初始容量16,负载因子0.75,意思是元素个数达到16 * 0.75 = 12时触发扩容,每次扩容翻倍。扩容时需要重新计算所有元素的位置,所以频繁扩容性能很差。笔试题里常让你算“HashMap初始化要传多大容量”,举个例子:如果你明确知道要存1000个元素,直接new HashMap<>(1000)够不够?不够,因为1000乘以0.75是750,还没到1000就触发扩容了。要避免扩容,初始容量应该至少是1000 / 0.75 + 1,大概1334,实际使用中很多人图省事直接传2000。
HashMap线程不安全这个点,笔试常和HashTable、ConcurrentHashMap对比。HashTable直接在方法上加synchronized,性能差到基本没人用。ConcurrentHashMap在JDK 8里用的是CAS + synchronized对桶加锁,锁粒度细,并发性能好。JDK 7的ConcurrentHashMap用了分段锁,这个区别偶尔也会考到,能说出来会很加分。
2.2 ArrayList与LinkedList的选型问题
ArrayList和LinkedList的区别也是一个经典考题。ArrayList底层是动态数组,查询通过下标直接定位,时间复杂度O(1),但插入和删除需要移动元素,平均O(n)。LinkedList底层是双向链表,插入和删除只需要修改指针,理论上是O(1),但前提是你已经定位到了那个节点。实际代码里,比如list.get(500)这种操作,LinkedList需要从头或尾遍历,复杂度O(n),反而比ArrayList慢得多。
有个比较经典的笔试问法:LinkedList真的更适合“频繁插入删除”吗?答案是要看插入位置。如果是在尾部插入,ArrayList只需要在数组末尾追加,偶尔触发扩容就够了,实际性能不一定比LinkedList差。如果是在头部或中间插入,ArrayList要批量移动元素,确实慢,LinkedList才有优势。所以选型不能背结论,要根据数据规模、访问模式、插入位置来综合判断。
我在看候选人代码时,最怕的就是那种无论什么场景一律new ArrayList<>()的写法,虽然大多数时候不出错,但一旦数据量上来,性能问题会非常明显。笔试里如果能写出“根据场景选择集合”的分析思路,而不是死记硬背,通常能拿到不错的分数。
2.3 线程安全集合与fail-fast机制
这部分笔试常考两个点:CopyOnWriteArrayList和ConcurrentHashMap的实现思路,以及迭代时的fail-fast机制。CopyOnWriteArrayList的核心思想是写时复制:修改操作先复制一份底层数组,在副本上改,改完再把引用指向新数组。读操作不加锁,适合读多写少的场景,代价是每次写入都要复制数组,频繁写会非常耗内存。
fail-fast机制值得展开说。ArrayList、HashMap这些非线程安全集合,迭代过程中如果结构被修改,会抛出ConcurrentModificationException。实现原理是迭代器内部维护一个modCount(修改次数)字段,每次迭代都会检查它是否和预期值一致,不一致就抛异常。注意,fail-fast并不是一种并发安全保证,它只是“尽量快速失败”,帮你在开发阶段发现问题。笔试里常问“怎么避免这个异常”,常规做法是遍历时不要直接remove,改用迭代器的remove方法,或者用removeIf,在并发场景下换用线程安全集合。
2.4 集合笔试实战:一道典型的HashMap题目
结合上面这些点,很多公司会出这样一道题:HashMap中使用String作为key和自定义对象作为key,有什么区别?这题考的是hashCode和equals的契约。如果自定义对象没有重写hashCode,那么不同实例即使内容相同,哈希值也不同,会导致get的时候根本找不到之前put进去的值。如果重写了hashCode但没重写equals,两个内容相同的对象哈希值一样,但equals返回false,在链表或红黑树里查询时会因为比较失败而查不到。
正确的做法是两者同时重写,并且保证“equals相等的两个对象,hashCode一定相等”。反过来“hashCode相等的两个对象,equals不一定相等”,这就是哈希冲突。还有一个小细节是重写equals时要用getClass判断类型,不要用instanceof,否则和子类比较时容易出现不对称问题。这些都是在实际编码中会遇到的坑,笔试考它们真不是刁难人。
3. 并发与多线程:读懂题意比背API重要
并发编程是Java后端笔试的深水区,也是真正区分“背书选手”和“理解选手”的地方。原因很简单:并发问题在生产环境一定会遇到,且出了问题极难排查,面试官自然要在这个环节重点考察候选人。
3.1 线程创建方式与任务提交方式
笔试常问“创建线程有几种方式”,标准答案一般说三种:继承Thread、实现Runnable、实现Callable配合FutureTask。但更推荐的说法是两种:直接创建线程和通过线程池创建任务。实际开发中没人会去new Thread裸奔,因为线程的创建和销毁成本很高,而且无限制创建线程会导致系统资源耗尽。
Runnable和Callable的核心区别是:Runnable的run方法没有返回值,也不能抛出受检异常;Callable的call方法有返回值,可以抛异常。笔试有时会问“线程池提交任务后如何获取结果”,答案是用Future接收返回结果,调用get()方法阻塞等待。这里有个常见的坑:多个任务提交后,如果你先对第一个任务调用get(),而它执行很慢,后面的任务即使已经完成也无法及时拿到结果。实际处理时建议先全部提交,再统一获取结果,或者用CompletableFuture做异步编排。
3.2 synchronized与ReentrantLock的对比
这块笔试几乎必考。先明确两者的相同点:都是可重入锁,都可以实现线程同步。不同点体现在几个方面:synchronized是关键字,由JVM层面实现,使用简单,不需要手动释放锁;ReentrantLock是JDK提供的类,需要手动加锁和解锁,unlock最好放在finally里。功能上ReentrantLock更灵活,支持公平锁与非公平锁切换、支持超时获取锁、支持多条件变量Condition。性能上,JDK 6之后两者差距不大,早期synchronized性能差是因为重量级锁的实现方式,后来引入了偏向锁、轻量级锁、锁升级机制,性能已经追平了。
这里补充一个笔试容易踩坑的知识点:synchronized加在静态方法和实例方法上的区别。加在静态方法上锁的是Class对象,加在实例方法上锁的是当前实例对象。如果两个线程一个调静态方法、一个调实例方法,它们用的是不同的锁,不会互斥。这个细节我见过不止一次在笔试里出现,答错的人不少。
3.3 volatile的可见性与原子性问题
volatile是并发考点里被误解最多的一个关键字。很多人背了“保证可见性,不保证原子性”这句话,但不知道“可见性”到底指什么。要理解可见性,得先知道Java内存模型:每个线程有自己的工作内存,操作变量时先把主内存的值拷贝到工作内存,操作完再写回主内存。如果多线程同时操作同一个变量,这个“拷贝-修改-写回”的过程对彼此不可见,就会出现线程A改了值,线程B还在用旧值的情况。volatile的作用就是强制线程每次读写都直接操作主内存,保证一个线程修改后,其他线程立即可见。
但volatile解决不了复合操作的原子性问题。典型的例子是count++,这个操作拆开来看是“读取、加一、写回”三步,多个线程同时执行时仍然会丢数据。笔试答案里一定要点明:volatile适合“一个线程写、多个线程读”的场景,不适合“多个线程同时写”的场景。要保证原子性,得用AtomicInteger或者synchronized。AtomicInteger底层用的是CAS(比较并交换),CAS有三个操作数:内存位置、预期原值、新值,只有内存位置的值等于预期原值时才更新为新值,否则不修改并重新读取。这个机制面试官也爱追问,建议把ABA问题的概念也顺带看一下,AtomicStampedReference就是用来解决ABA问题的。
3.4 线程池参数与拒绝策略的底层逻辑
线程池是后端开发天天打交道的东西,笔试的考法很直白:给一个ThreadPoolExecutor的构造方法,问每个参数什么含义,或者问“核心线程数是5,最大线程数是10,队列容量是100,现在来了200个任务,会发生什么”。
标准流程要记清楚:提交任务时,如果当前线程数小于核心线程数,创建新线程执行;如果大于等于核心线程数,优先把任务放进队列;如果队列满了,再判断是否还能创建非核心线程;如果线程数已经到最大值且队列也满了,就执行拒绝策略。注意一个易错点:线程池是要先塞队列,不是先创建非核心线程,很多人搞反了这个顺序。
拒绝策略有四种:AbortPolicy直接抛异常,这是默认的;CallerRunsPolicy让提交任务的线程自己执行这个任务;DiscardPolicy静默丢弃;DiscardOldestPolicy丢弃队列里最老的任务。笔试问“你们项目里用的哪种”时,不能只说默认抛异常,最好能结合业务说明为什么选它。比如说你希望丢任务时至少有个日志报警,就自定义一个拒绝策略,在拒绝时记录当前队列大小和任务信息。
3.5 一道并发笔试真题的完整推演
我拿一道比较经典的题来演示整个思考过程:两个线程交替打印1到100,一个线程打印奇数,一个线程打印偶数,用synchronized实现。
思路是这样的:定义一个共享的number变量,再定义一个共享的锁对象。每个线程进入循环后先获得锁,判断当前number的奇偶性是否是自己负责的,不是就调用wait()释放锁并等待;是自己负责的,就打印并number++,然后调用notifyAll()唤醒对方线程。这里有两个关键点:一是判断条件必须用while而不是if,因为线程被唤醒后要重新检查条件是否满足,防止虚假唤醒;二是wait和notify调用前必须持有同一个锁对象,否则会抛IllegalMonitorStateException。
这种题在笔试里以手写代码出现时,很多人能写出大概,但漏了while条件或者漏了notify调用。这两个细节恰恰是实际并发编程里最容易出问题的点,建议反复练习到条件反射的程度。
4. JVM与内存机制:高频但容易丢分
JVM相关内容在笔试里属于“人人都知道要考,但大部分人只背了个大概”的模块。内存区域划分、垃圾回收算法、类加载过程,这些确实是常客,但考法越来越细。如果你只背了“堆存对象、栈存引用”这句话,遇到稍微深入的题就容易卡住。
4.1 运行时数据区的划分与职责
JVM运行时数据区常考点包括:程序计数器、虚拟机栈、本地方法栈、堆、方法区(JDK 8后元空间取代了永久代)。笔试爱考的是“哪些区域是线程共享的、哪些是线程私有的”。线程私有的是程序计数器、虚拟机栈、本地方法栈;共享的是堆和方法区。程序计数器是当前线程执行的字节码行号指示器,唯一一个不会出现OOM的区域。虚拟机栈里保存的是栈帧,每个方法调用对应一个栈帧的入栈和出栈,里面包含局部变量表、操作数栈、动态链接、方法出口等信息。
这里有个常见的笔试陷阱:“局部变量到底存在哪里?”如果局部变量是基本类型,就存在虚拟机栈的局部变量表里;如果是引用类型,引用存在于栈里,对象本身在堆里。很多人背“栈里存局部变量”后,遇到引用类型就回答错误。还有一个容易混淆的是“方法区存什么”,简单说存类元信息、常量、静态变量、JIT编译后的代码等。JDK 8把字符串常量池挪到了堆里,把类的元数据放到了元空间,元空间使用的是本地内存而不是JVM堆,这个改动笔试偶尔会问。
4.2 垃圾回收算法与收集器选型
垃圾回收这块,笔试重点是判断对象是否已死,以及几种回收算法的原理。判断对象已死主流方法是可达性分析,从GC Roots出发向下搜索,不可达的对象会被回收。GC Roots包括:虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象、本地方法栈中JNI引用的对象等。引用计数法因为循环引用问题被放弃了,这个点笔试经常考。
回收算法里,标记-清除有碎片化问题,复制算法浪费空间但效率高,标记-整理解决碎片化但移动对象成本高。现代JVM的分代收集策略就是组合使用:新生代频繁创建对象,用复制算法,把内存按8:1:1分成Eden和两个Survivor区;老年代对象存活时间长,用标记-整理或标记-清除。笔试偶尔会问“为什么是8:1:1”,这是兼顾空间利用率和回收效率的经验值。
收集器方面,笔试不会让你背所有收集器的参数,但至少要理解CMS和G1的核心设计。CMS的目标是缩短停顿时间,它的工作流程分初始标记、并发标记、重新标记、并发清除四个阶段,其中初始标记和重新标记会STW,但时间很短。CMS有两个明显的缺点:并发清除阶段会和应用线程抢CPU,导致吞吐量下降;浮动垃圾没法在本次GC处理,只能留给下一次。G1把堆分成多个Region,维护一个优先级列表,每次回收价值最大的Region,这打破了传统分代的物理隔离。JDK 9之后G1成为默认收集器,笔试如果问默认收集器,别再答Parallel+CMS了。
4.3 类加载过程与双亲委派模型
类加载机制常考“一个类的完整生命周期”和“双亲委派模型”。生命周期包括加载、验证、准备、解析、初始化、使用、卸载。需要注意的细节是:加载不等于初始化,只有主动使用才会触发初始化,比如new对象、调用静态方法、访问静态字段、反射、初始化子类时父类未初始化等。被动使用不会触发初始化,比如通过子类访问父类的静态字段不会初始化子类,用数组定义引用不会初始化类,引用常量不会触发初始化。
双亲委派模型的意思是:类加载器收到加载请求后,先让父类加载器尝试加载,父类加载器加载不了才自己加载。这样做的核心目的是防止核心类库被篡改,比如你自己写了一个java.lang.String,最终会被引导类加载器加载系统自带的String,你写的类根本不会被加载。笔试常问“能不能自己写一个和java.lang.String同包同名的类”以及“如何打破双亲委派模型”。打破的方式是重写loadClass方法而不遵循父优先逻辑,比如Tomcat的WebAppClassLoader就是先自己加载Web应用下的类,找不到再交给父加载器,这样可以让不同应用加载同一依赖的不同版本,实现隔离。
4.4 常见OOM场景与排查思路
笔试或面试常给一个报错场景,问你是什么原因、怎么排查。堆内存溢出通常是对象太多或存在大对象,排查时先看-Xms和-Xmx设置是否合理,再用jmap导出堆转储文件,配合MAT分析哪个对象占用的内存最多。GC overhead limit exceeded是系统一直在GC但回收效果很差,背后往往是死循环创建对象或者集合无限增长。元空间溢出常见于生成了大量动态代理类或CGLIB代理类的场景。线程栈溢出一般是递归调用太深或死递归,解决方法是检查递归终止条件,或者通过-Xss调大栈容量,但调大只是缓解,关键还是要避免无界递归。
我建议复习JVM时动手跑一遍jstat、jmap、jstack这些命令,不一定要分析得多深入,但要熟悉输出结果长什么样。笔试选择题里会出现这些命令,完全没见过的人容易懵,见过一次就能选对。
5. Spring核心:框架考点从IOC/AOP延展
现在的后端岗位笔试,Spring基本是必考的。原因很简单,大部分公司做业务开发都在用Spring Boot,框架理解不到位,很难设计出可维护的系统。这块的考法很有特点:不直接问“什么是IOC”,而是让你结合Bean生命周期、循环依赖、事务失效场景来分析问题。
5.1 IOC容器与Bean的生命周期
IOC(控制反转)的核心思想是:对象的创建和管理交给容器,开发人员只声明依赖关系。笔试里常配一道代码题,问“Bean的构造方法、@PostConstruct标注的方法、InitializingBean的afterPropertiesSet、BeanPostProcessor的postProcessBeforeInitialization,这些方法执行的先后顺序”。
完整顺序大概是:实例化Bean,填充属性,执行BeanNameAware、BeanFactoryAware等回调,执行BeanPostProcessor的postProcessBeforeInitialization,执行@PostConstruct,执行afterPropertiesSet,执行自定义的init-method,执行BeanPostProcessor的postProcessAfterInitialization。这里有个容易忽略的地方:@PostConstruct的执行顺序在afterPropertiesSet之前,很多人一紧张就答反了。记忆技巧是先执行JSR规范的注释,再执行Spring接口方法,最后执行配置文件里指定的init方法。
循环依赖也是高频考点。Spring解决setter注入或字段注入的循环依赖靠的是三级缓存:一级缓存放已经创建完成的对象,二级缓存放提前暴露的早期对象,三级缓存放对象工厂。当A依赖B,B又依赖A时,A先实例化并暴露一个早期引用,B在创建时可以拿到这个早期引用完成创建,B创建完后A再从缓存拿到完整的B进行属性填充。但要注意,构造器注入的循环依赖无法解决,因为实例化时就需要对方,这时没有早期引用可用。笔试如果问“Spring能不能解决构造器循环依赖”,答案是不能,需要改用setter注入或@Lazy。
5.2 AOP实现原理与代理选择
AOP这块常考Spring AOP的底层实现。如果目标类实现了接口,Spring默认用JDK动态代理,代理类和目标类实现同一个接口,调用时会进入InvocationHandler的invoke方法,在方法前后织入增强逻辑。如果目标类没有实现接口,Spring用CGLIB生成目标类的子类,通过重写方法来实现增强。JDK 7之后CGLIB性能已经不算差,而且Spring Boot 2.x开始默认用CGLIB代理,不再要求目标类必须实现接口。
笔试考过一道题:“Spring AOP中,同一个类里的方法A调用方法B,A上面有@Transactional,B上面也有@Transactional,事务会不会生效?”答案是B上的事务不生效。原因是Spring AOP基于代理对象调用,方法A内部直接调用方法B时,调用的是原始对象的方法,不是代理对象的方法,所以增强逻辑没有机会切入。解决方法是把B拆到另一个Bean里,或者通过AopContext.currentProxy()拿到代理对象再调用。这个知识点在实际开发中踩坑率极高,我在代码评审里提醒过无数次。
5.3 事务失效的典型场景
事务这块笔试很少直接问“事务的隔离级别”,而是问“哪些情况会导致@Transactional失效”。要把这类题答全,需要从三个层面想。
第一,方法调用层面:事务方法被同类内部方法调用,不走代理,不生效,这个前面刚提过;方法不是public,Spring事务默认只拦截public方法。
第二,异常处理层面:@Transactional默认只对运行时异常回滚,受检异常不会回滚。如果你在方法里try-catch把异常吞掉了,事务同样不会回滚。这个点极其重要,很多线上数据不一致问题都是“捕获异常没抛出”导致的。
第三,传播行为层面:如果外层方法没有事务,内层方法默认加入外层事务,这就是REQUIRED传播行为。如果设置了REQUIRES_NEW,内层方法会挂起外层事务,开启新事务,commit和rollback相互独立。笔试喜欢给你一个嵌套方法调用的场景,让你判断异常发生时哪些数据被回滚、哪些没回滚,画一张事务传播的表格会帮助理解。
5.4 Spring Boot自动配置的核心逻辑
Spring Boot的自动配置是面试官很喜欢问的点,因为它能看出你是“只会用框架”还是“理解框架”。自动配置说白了就是:Spring Boot在启动时根据classpath下的依赖、配置文件和已有的Bean,自动创建一批默认的Bean。核心组件有三个:@EnableAutoConfiguration注解、spring.factories或AutoConfiguration.imports文件里声明的自动配置类、以及@Conditional系列条件注解。
笔试常问“自动配置类上的@ConditionalOnMissingBean是什么意思”。意思是当前容器中不存在指定类型的Bean时,才创建自动配置的默认Bean。这个设计给开发者留了覆盖的口子:你想替换默认实现,就自己定义相同类型的Bean,自动配置会退位让贤。比如你想自定义一个RestTemplate,直接声明一个@Bean覆盖就行了,不用改框架代码。另一个常考点是“Spring Boot的启动过程”,从SpringApplication.run开始,经历推断Web应用类型、加载自动配置类、创建并刷新容器、启动内嵌Web服务器等步骤。复习时建议把spring.factories文件实际打开看一眼,对理解会有很大帮助。
6. 复习策略与刷题方法:把知识点转化成解题能力
知识点掌握是一回事,拿到题能读懂问什么、能踩中得分点,是另一回事。我见过不少基础不错的同学,笔试分数反而不高,问题出在复习方法上。他们不是不懂,而是没有针对笔试这种考察形式做专门训练。
6.1 高频考点与复习优先级
根据我出题和面试的经验,可以按出现频率把考点分成梯队。第一梯队是HashMap与集合框架、线程池与并发机制、JVM内存与GC、Spring事务与AOP,这四个方向占据了笔试题量的半壁江山,必须优先搞定。第二梯队是Java基础细节(自动装箱、字符串、异常)、类加载机制、设计模式、网络协议基础(HTTP、TCP)。第三梯队才是一些比较偏的考点,比如自定义类加载器、JMX、字节码增强等,这部分不必花太多时间,尤其是时间有限的时候。
我的建议是不要按教科书顺序从第一章看到最后一章,而是先拿几套往年真题,把错题对应的知识点标出来,按考频从高到低排列,优先攻薄弱的高频点。后端知识点很散,没有优先级地乱看,一个月下来感觉啥都学了,但啥都记不牢。
6.2 用代码验证结论,不要只背结论
笔试复习最容易踩的坑就是“背结论不写代码”。比如前面提到的Integer ==比较、类初始化顺序、volatile可见性问题,你光看解析觉得懂了,真到了笔试换一种问法就认不出来。我强烈建议把每一个涉及运行结果的考点都敲一遍代码,实际跑出结果,再想为什么是这个结果。多跑几次之后,那些“反直觉”的点就会变成直觉。
还有一个小技巧:改造代码验证自己的理解。比如你理解了Integer缓存范围是-128到127,可以试试把JVM参数-XX:AutoBoxCacheMax调大,再看==的结果变化。亲手验证过的知识点,记忆深度远高于看十遍文档。
6.3 错题整理与模拟练习的节奏
每周至少安排一次限时刷题,一轮做下来把错题分类标记:是概念不清、细节遗漏、还是审题失误。概念不清的回到原文档补基础,细节遗漏的单独记下来反复看,审题失误的就要提醒自己读题慢一点。我在准备笔试时会把错题按这个逻辑整理,效果比盲目刷新题好得多。
Java后端笔试的覆盖面确实很广,但它的考察逻辑是有规律的:基础考深度,框架考理解,并发靠动手验证。这篇先整理了Java基础和框架集合、并发、JVM、Spring这几大块的核心考点和复习思路,后续系列里我会再针对网络协议、数据库、Redis、分布式这些方向做同样的拆解。知识点这东西,看懂只是第一步,能动手验证、能讲清为什么,才是真的吃透了。