说实话,收到“小满春招JAVA研发岗第三批笔试”通知的时候,我整个人是有点懵的。前两批笔试的动静我多少有所耳闻,群里陆陆续续有人晒题,结果到了第三批,通知异常安静,连个像样的考纲都没附。这意味着什么?意味着前两批筛掉了一大批人,第三批是要真正动真格的了。我是认认真真把这场笔试当作一次“技术体检”来对待的,考完之后花了整整两天复盘,今天这篇就是把这次笔试的完整经过、题目类型、踩坑过程、以及我后续补课的方向全部整理出来。
这份复盘适合谁看?如果你正在准备Java研发岗的春招、秋招,或者想检验一下自己的Java基础功是否扎实,这篇文章都能给你一个非常具体的参考坐标系。我这里不会只贴“考了什么”,更会把每类题目背后的考点逻辑、我当时是怎么想的、哪些地方栽了跟头、后来怎么解决的,一条条拆开讲清楚。
1. 笔试通知与考前准备:第三批到底在考什么
笔试之前我专门去翻了前两批的经验帖,发现一个规律:第一批侧重基础筛选,题型以选择题和简单填空题为主;第二批开始出现手写代码和场景设计;到了第三批,题目明显更“活”了,很多题不是靠背八股文就能答出来的,需要你真正写过代码、踩过坑、知道原理背后的权衡。
1.1 笔试形式与题型分布
先说笔试形式。第三批用的是在线笔试平台,全程摄像头监控,限时三小时,题型分布大概是这样的:
| 题型 | 题量 | 分值占比 | 考察重点 |
|---|---|---|---|
| 单选题 | 20道 | 30% | Java基础、集合框架、JVM、并发 |
| 填空题 | 5道 | 10% | 代码输出结果、补全代码片段 |
| 手写代码题 | 3道 | 40% | 排序算法、模拟题、Spring Boot场景题 |
| 简答题 | 2道 | 20% | 问题排查思路、方案设计 |
这个分值分布本身就说明了一个问题:手写代码和简答题占了60%,也就是说,这场笔试的核心目的不是看你“知不知道”,而是看你“会不会用”。
1.2 考前我做了什么准备
坦白讲,考前一星期我把大部分精力放在了算法和框架上,Java基础的八股文反而是碎片时间过的。复习资料用的是网上常见的Java面试题整理、Java八股文合集,外加自己整理的一份易错点笔记。
环境准备上也踩了个小坑。考试要求本机安装JDK,我本机是JDK 17,IDE用的是VSCode。为了保险起见,我提前在VSCode里装好了Java Extension Pack、Maven插件,并且手动把系统环境变量里的JAVA_HOME指到了JDK 17的安装路径。这步操作看似基础,但真的救了我一命——后面笔试过程中遇到编译报错,环境问题占了好大一部分原因。
考前最后一天我做的三件事,个人认为比盲目刷题更有效:第一,手写了两遍快速排序和冒泡排序;第二,把Java集合类的继承结构画了一遍;第三,把Spring Boot常见的注解和拦截器流程过了一遍。事实证明,这三件事基本覆盖了笔试里最核心的几道题。
2. Java基础题复盘:八股文里被反复拷打的那些点
单选和填空题基本集中在Java基础上,说句实话,这些题不算偏,但出题人很会在“容易混淆”的地方设陷阱。如果你只是粗略看过八股文,没有真正动手验证过,很容易在几个选项之间摇摆。
2.1 面向对象、运算符与表达式的陷阱
有一道题我印象很深,考察的是面向对象中的“向上转型和动态绑定”。题目给了一段代码,父类引用指向子类对象,调用一个父类和子类都有的实例方法,问输出结果。关键在于:编译看左边,运行看右边。这个知识点本身不复杂,但出题人把静态方法也掺杂进去了,静态方法是“编译看左边,运行也看左边”,和实例方法正好相反。如果你把这两者搞混,这道题必错。
运算符和表达式方面,重点考察了短路运算和位运算。题目问的是int a = 5; boolean b = (a > 3) && (a++ > 5);执行后a的值。很多人会下意识觉得a变成了6,实际上由于短路机制,a++根本没有执行,所以a还是5。这题考的不是你会不会算,而是你有没有仔细学过Java运算符的执行顺序。
++i和i++的混合运算也出现了,比如int i = 2; int j = i++ + ++i;,我当时算出来是5,但说实话犹豫了一下。这类题没什么技术含量,但很容易在考场上因为紧张而算错,建议大家考前还是把自增自减的底层逻辑再捋一遍:i++是先用后加,++i是先加后用。
位运算考了一道&和&&的区别,以及按位异或的一个小例子。&&有短路效果,&没有,这个属于基础中的基础,但如果复习不仔细,考场上还是有可能在“输出结果类填空题”里翻车。
2.2 枚举类型、数组越界与标识符命名规则
枚举类型这个考点,我估计很多人会轻视。笔试出了一道关于枚举的填空题:给一个枚举类型Color,里面定义了RED, GREEN, BLUE,问Color.RED.name()和Color.RED.toString()的输出是否一样。这道题实际上在考Enum类的name()和toString()方法默认行为,两者默认返回的都是枚举常量的名称,也就是"RED"。但如果枚举类里重写了toString(),结果就会不同。
还有一道题是关于枚举实现单例模式的,问这种方式能否保证线程安全。答案是能,而且Enum实现的单例还能防止反射攻击和序列化破坏,这是《Effective Java》里推荐的方式。这题已经超出基础范畴了,需要有足够的源码阅读量才能答得出来。
数组越界异常考得比较直接,给了一个循环,问第几次迭代会抛出ArrayIndexOutOfBoundsException。关键在于看清楚循环的边界条件,比如for (int i = 0; i <= arr.length; i++)这种经典错误,最后一定会越界。这道题不难,但它提醒了我一个事——越界异常不仅仅是“写错了”的问题,它背后是Java数组在内存中的连续空间分配机制,访问超出索引范围就必然抛异常,这是JVM的边界保护机制。
标识符命名规则也出现了,核心考点是:Java标识符只能以字母、下划线、美元符号开头,不能以数字开头,且不能是Java关键字。题目给了一个看起来很像合法标识符的选项,实际上里面混入了关键字class,这时候就需要细心辨析。
3. 手写算法题:排序实现和一道“列车调度”模拟题
这一部分是我觉得整场笔试里最有含金量的。三道手写题分别是一道排序算法、一道模拟类题目、一道面向框架的代码设计题。排序题是基础,模拟题考的是建模能力,框架题考的是真实项目的开发经验。
3.1 冒泡排序和快速排序的手写实录
第一道手写题要求完整写出冒泡排序和快速排序的实现。
冒泡排序本身不复杂,但出题人要求尽量优化。于是我在基础版冒泡排序上加了“是否发生交换”的标记位,如果某一轮遍历没有发生任何交换,说明数组已经有序,直接跳出循环。这个优化能把最好情况的时间复杂度从 O(n²) 降到 O(n)。以下是我当时提交的代码:
public void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) return; for (int i = 0; i < arr.length - 1; i++) { boolean swapped = false; for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; swapped = true; } } if (!swapped) break; } }快速排序这道题,我选择了经典的“双边指针+基准值”写法。考场上最怕的其实是边界条件写错,比如左指针越过右指针、递归区间变成无限循环。我当时的写法是:
public void quickSort(int[] arr, int left, int right) { if (left >= right) return; int pivot = arr[left + (right - left) / 2]; int i = left, j = right; while (i <= j) { while (arr[i] < pivot) i++; while (arr[j] > pivot) j--; if (i <= j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; i++; j--; } } if (left < j) quickSort(arr, left, j); if (i < right) quickSort(arr, i, right); }这个写法和教科书上的略有不同,它是“挖坑法”和“指针交换法”的结合。实际写下来,最需要注意的是取基准值的方式。如果固定取最左边,数组已经有序的时候效率会很差;取中间位置的值,在一定程度上可以减少最坏情况的概率。笔试后我反思了一下,如果当时再加上一句注释说明“随机选取基准”的优化思路,效果会更好。
3.2 列车调度题目解题思路
这道题我得多说几句,因为它非常典型。题目大致是这样:有一组列车,按照某个顺序依次到达调度站,每列车只能从入口进入、从出口驶出,调度站里有若干条平行的轨道,列车进入一条轨道后就不能再移动,而且要求每条轨道上的列车按编号严格递增。问至少需要几条轨道才能完成全部列车的调度。
如果第一次遇到这个题,很容易被它的迷惑性描述带偏。实际上,它和“LIS(最长递增子序列)”问题有非常紧密的联系。答案是:最少需要的轨道数等于原序列的最长递增子序列长度。为什么?因为每条轨道上的列车编号必须递增,这就相当于把原序列拆分成若干个递增子序列,要想分割数最少,根据 Dilworth 定理,最少分割数就等于最长递减子序列的长度,放到这个场景里,也就是需要把输入序列倒过来看。
我当时用的解法是贪心+二分。维护一个数组tails,tails[i]表示长度为 i+1 的递增子序列的末尾元素最小值。遍历每一列车的编号,在tails中二分查找第一个大于等于当前编号的位置,如果找到就覆盖它,否则就追加到尾部。最终tails的长度就是最少需要的轨道数。
我的实现大概是这样:
public int minTracks(int[] trains) { int[] tails = new int[trains.length]; int len = 0; for (int x : trains) { int left = 0, right = len; while (left < right) { int mid = left + (right - left) / 2; if (tails[mid] >= x) { right = mid; } else { left = mid + 1; } } tails[left] = x; if (left == len) len++; } return len; }这道题能答出来,实际上靠的是平时做题的积累。如果只是死记硬背“贪心+二分”的模板,考场上很容易卡在“为什么第 5 列列车不能放到第 1 条轨道上”这种细节上。建议所有准备笔试的同学,遇到这种题一定要把背后的定理和推导关系搞清楚,不然换一个壳子,你照样认不出来。
4. 编程环境与编译报错:笔试现场最容易被卡住的地方
这一部分虽然不是笔试题本身,但我想重点说,因为环境问题在笔试中造成的干扰,可能比题目难度对成绩的影响还大。我当时在VSCode里跑代码就连续碰到了两个报错,一个是关于JDK版本的,一个是关于乱码的,网上搜索量都很高,说明不少人遇到过。
4.1 “源发行版 17 需要目标发行版 17”的解决办法
我在写代码的时候遇到了这个报错:java: 警告: 源发行版 17 需要目标发行版 17。中文环境下它还有变体:java: 无效的源发行版: 17。这个问题的本质是:编译器版本、项目语言级别和实际使用的JDK之间不一致。
我用的是VSCode + Spring Boot插件,出现这个报错的时候,我的JAVA_HOME已经设成了JDK 17,但项目的pom.xml里没有显式指定maven.compiler.source和maven.compiler.target,导致Maven默认使用了较低级别的编译参数。解决方法是把这两个属性明确设置为17:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties>另外,如果你用的是IDEA,还需要检查Project Structure里的Project SDK和Project language level是否一致。很多时候“源发行版 17 需要目标发行版 17”报错,就是这两个地方没对齐。
4.2 VSCode运行Java报错乱码问题
笔试过程中我在控制台输出中文时出现了乱码,这个问题的根源是文件编码和控制台编码不一致。VSCode默认的UTF-8和Windows控制台的GBK冲突,导致中文输出变成了一堆看不懂的符号。
我的解决办法是修改控制台的编码设置:在launch.json或者settings.json里增加控制台编码为UTF-8的配置项。如果你用的是javac命令行运行,也可以加-encoding UTF-8参数来编译。这个坑非常容易被人忽视,但一旦出现极其影响心态——你辛辛苦苦手写完了代码,结果连输出都看不清楚。
4.3 OutOfMemoryError: insufficient memory 的处理
有一道简答题问到线上应用报java: outofmemoryerror: insufficient memory应该怎么排查。这个问题是JVM运行时内存不足的典型信号。我当时给出的排查思路是:先确认是哪种内存区域溢出,再结合日志和监控工具定位GC情况。
网上关于这个报错的热搜词很多,表述也各不相同,但真实开发中,我们通常从以下几个维度去排查:
| 排查方向 | 具体手段 |
|---|---|
| 堆内存溢出 | 查看堆内存使用情况,考虑是否调整 -Xmx 参数 |
| GC压力过大 | 通过GC日志分析频繁Full GC是否导致无法分配新对象 |
| 内存泄漏 | 使用MAT或VisualVM分析堆转储文件 |
| 系统内存不足 | 检查服务器物理内存是否充足,是否有其他进程占用了大量内存 |
如果是新生代对象大量堆积,就要考虑是不是有类似“一次性从数据库查询了大量数据”的代码;如果是老年代持续增长且无法回收,优先怀疑持有静态集合或缓存没有清理。我当时的回答还补充了一点:除了调整JVM参数,还应该排查应用代码里是否存在未关闭的资源、无界队列、以及使用ThreadLocal后没有清理导致的对象引用链。
5. Java高级特性与框架实战考点:从lambda到Spring Boot
这一部分主要考察代码功底和框架应用水平。看起来是选择题和简答题,实际上背后考的是你对Java语言特性和常用框架的理解深度。
5.1 lambda表达式与Comparator.comparing排序技巧
有一道填空题给了这样的场景:一个学生类有姓名和成绩两个字段,要求用一行代码实现按成绩降序排列,成绩相同的按姓名升序排列。这个题如果是用传统匿名内部类,代码会非常啰嗦,但如果使用lambda表达式和Comparator.comparing就能优雅地解决。
我当时给出的写法是:
students.sort( Comparator.comparing(Student::getScore).reversed() .thenComparing(Student::getName) );但这里有一个非常容易踩的坑:Comparator.comparing(...).reversed()的reversed()只作用于前一个比较器,如果你写成了Comparator.comparing(Student::getScore).reversed().thenComparing(...),那么thenComparing是在整个逆序比较器的基础上继续连接的,这一点不会倒过来影响成绩的排序。也就是说,这个写法是:先按成绩降序,成绩相同再按姓名升序。有没有问题?没有。但如果需求是“先按成绩升序,再把整个结果反转”,就要用Comparator.comparing(...).thenComparing(...).reversed()这种整体反转的写法。
这类题靠死记硬背不靠谱,实际写代码时一两次就会记住,因为报错或者结果不符合预期的时候你会主动去查。
5.2 Spring Boot API Key安全对接
简答题里有一道非常实战的题:两台系统之间通过Spring Boot接口做数据对接,需要保证接口只被授权的客户端调用,你会怎么设计?我当时把回答拆成了三层。
第一层,使用API Key作为身份凭证。客户端在请求头中携带API Key,服务端用拦截器统一校验。当时我画了这样一个思路:
@Component public class ApiKeyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String apiKey = request.getHeader("X-API-Key"); if (apiKey == null || !apiKey.equals("expected-key")) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } return true; } }第二层,考虑到API Key本身存在泄露风险,更稳妥的做法是加上签名校验。客户端用密钥对请求参数和时间戳进行签名,服务端用同样的方式验签,同时校验时间戳防止重放攻击。第三层,使用HTTPS保证传输过程安全。三层叠加,才能算一个比较可靠的安全对接方案。
5.3 接口自动化测试框架的选型与设计
笔试里还问到了Java接口自动化测试框架。这道题其实更多是开放性的,考察你对测试工具链的了解程度。我当时从主流方案切入,整理了几个点:请求层用Apache HttpClient或者OkHttp,断言层用TestNG/JUnit 5,数据层用TestNG DataProvider或JUnit 5的参数化测试来驱动多组用例。整个框架可以围绕“用例数据 -> 请求执行 -> 断言校验 -> 报告输出”这条链路来搭建,CI/CD集成上也相对方便。
这个方向由于不是我的主攻领域,我答得不算深,但基本把思路理清了。后来查资料发现,如果想往全栈方向靠,可以把REST Assured作为核心库,配合Allure报告和Jenkins定时任务,做成一个可持续运行的接口测试平台。
6. 笔试后的反思:八股文、Spring Boot、JVM该怎么继续补
笔试结束后我花了很长时间复盘,最大的感触是:现在的笔试已经不再是“背多分”了。它越来越像一场真实的开发模拟——题目本身并不深,但需要你有真实的项目经验、真实的环境排错能力,以及面对不熟悉问题时冷静拆解的思维习惯。
6.1 八股文到底要不要背
关于“Java面试八股文”,我的观点是:要背,但不能只会背。八股文本质上是把一个个知识点压缩成“最干”的形态,它提供了一个知识框架,确保你在面试和笔试时不会漏掉关键点。比如面向对象、集合、JVM、并发,这些内容不去系统梳理一遍,心里很难有底。
但是,光背八股文远远不够。笔试里的很多题目,比如枚举单例、Comparator排序、源发行版报错排查,都是面向真实场景的。如果你没有真正写过程序、没见过这些报错、没配置过环境,就算背得滚瓜烂熟,遇到灵活的题照样无从下笔。我建议大家背完一条八股文之后,尽可能动手写一个小demo去验证它,用自己的代码去“内化”这条知识。
6.2 后续学习路线怎么安排
笔试之后,我给自己定了一个后续补课计划,分享出来供参考:
第一,Java基础再过一遍,重点看集合源码,尤其是HashMap在JDK 8之后的底层实现、红黑树的结构以及扩容机制。第二,JVM方面重点了解垃圾回收算法、常见GC组合、内存区域划分,最好能自己用VisualVM去观察一个简单应用的堆内存变化。第三,并发编程这一块,把synchronized、ReentrantLock、volatile、线程池的各个参数彻底搞清楚,并且理解AQS的底层设计。第四,Spring Boot方向,把自动配置原理、Spring IoC容器、AOP机制、拦截器与过滤器区别全部掌握,这样可以应对绝大多数后端岗位的要求。
整个学习过程不是靠“刷”而是靠“用”。比如学到线程池的时候,就可以自己写一个小应用模拟高并发下单,观察线程池的表现。
我笔试后还用了一个笨办法来巩固知识:把考场上没答出来的题,一道一道重新写一遍,然后把相关知识点做成思维导图。这个方法虽然老套,但对我来说非常有效,尤其是那个“列车调度”的贪心+二分解法,我在复盘时把推导过程又重新做了一遍,才真正理解了为什么答案是最长递增子序列的长度。
这次笔试给我最大的启示就是:你可以不会某个具体的知识点,但你不能没有解决问题的能力。Java的生态太大了,永远有你不熟悉的内容,但只要你掌握了扎实的基础,养成了自己动手验证的习惯,遇到任何题目都能在短时间内找到正确的方向。后面如果有人问我怎么准备这类春招笔试,我一定会说:先把八股文背熟,然后去写代码、去踩坑、去修bug,把每个“读过”变成“做过”,最后你会发现,笔试也不过是一场按部就班的开发任务而已。