每年三四月份都是Java岗求职的硬仗,今年小满的春招批次排得挺密,我能进第三批笔试也算运气。说实话,Java笔试这块儿,很多同学对着一堆“八股文”背得滚瓜烂熟,可真上了考场,发现出题人根本不按套路出牌,考的尽是些平时写代码根本不会注意的边角料。
这篇文章就把我这次小满JAVA研发岗第三批笔试的完整复盘写出来,不藏着掖着,从考点分布、典型题目、答题策略到踩坑经验,能写多细写多细。无论你是准备秋招的应届生,还是想跳槽的Java开发,只要能静下心看完,起码能少走半个月弯路。
1. 笔试整体拆解与考点分布
1.1 小满这次笔试到底想筛什么人
先说结论:小满第三批笔试的难度,整体属于中偏上,比一般中小厂要扎实,但比头部大厂的硬核算法题温和不少。它不追求让你做不出来,而是追求让你“会但不完全会”,专门挑基础功不扎实的软肋打。
整张卷子分四块:选择题(约30道)、填空题(约10道)、简答题(4道)和两道编程题。时间给了120分钟,说实话并不宽裕,尤其是编程题,如果前面选择填空犹豫太久,后面基本只能赶工。这点大家一定要有心理准备,后面我会专门讲时间分配。
从考点分布来看,Java基础占了差不多35%,集合框架15%,并发编程15%,JVM和内存10%,Spring/MySQL合计15%,剩下就是算法和杂项。注意一个明显的趋势:直接背概念就能拿分的题变少了,结合实际场景判断输出的题变多了。比如给你一段代码问输出什么、会不会报错、为什么,这种题最见功底。
我考完跟几个一起笔试的同学对了下答案,普遍反映选择题里“代码阅读题”最难,像异常处理流程、泛型擦除、字符串常量池这些,光靠背讲义根本扛不住,必须真的写过、踩过坑才有感觉。
1.2 为什么出题风格在悄悄变化
很多同学喜欢刷“八股文题库”,觉得背完就能横着走。但从小满这次笔试能明显看出来,出题人正在刻意规避那种“定义型题目”。比如很少直接问“什么是多态”,而是给你三个类,让你判断某个方法调用走的是哪个版本。
这种变化的背后逻辑其实很简单:公司招人是要干活儿的,不是招背书机器的。平时开发中遇到问题,能不能快速定位、能不能理解框架源码里的设计思路,这些能力在“背定义”的模式下根本测不出来。所以现在的笔试题越来越偏向考“代码行为”,就是给你一段可运行或不可运行的代码,让你推理它的行为。
比如有一道选择题,给了个try-catch-finally的代码块,finally里有个return,问最后返回什么。这种题在IDE里一跑就知道答案,但如果你没在脑子里“跑”过代码,光靠记忆很容易选错。出题人就是要用这种方式区分“真正写过代码的人”和“只刷过题的人”。
1.3 复习优先级怎么排
根据这次笔试的实际体验,我建议后面备考的同学按这个优先级来分配精力:
第一梯队(必拿分):Java基础语法、面向对象、异常处理、常用集合类、字符串相关。这些大概占50%的分数,难度不大,但要熟练到条件反射的程度。
第二梯队(拉分区):并发编程(synchronized、volatile、线程池参数)、JVM内存模型和GC、MySQL索引和事务、Spring Bean生命周期。这些是区分度最高的部分,也是简答题的重点来源。
第三梯队(锦上添花):算法与数据结构、设计模式、Linux命令、Redis基础。编程题和部分选择题会涉及,但不会太偏。
有一点要提醒:不要因为某块看着会就跳过。我这次就吃了亏,觉得枚举类型简单没细看,结果选择题里考了枚举的values()方法和ordinal()的关系,选项挖得特别细,直接给我整不会了。
2. 核心考点逐个拆解
2.1 Java基础与面向对象:别以为你都会
这回笔试的Java基础部分,覆盖面很广,但都从最基础的点上做了延展,专门考你理解的“深度”。比如运算符优先级,这个大学里学完就忘的东西,竟然出现在选择题里,问你int a = 1; int b = a++ + ++a;最终b等于多少。这类题看着简单,但里面既有自增运算符的执行时机,又有优先级和副作用的问题,不仔细真的会算错。
再比如Lambda表达式,那几年Java 8的Lambda刚出来时大家都觉着新鲜,现在Lambda已经成了开发标配。笔试里考的是一道List排序的题,要你用Lambda实现按某个字段倒序排列。这题看着不难,但要注意Comparator.comparing的用法。如果平时没写过comparing(Foo::getBar).reversed()这种链式写法,很容易在.reversed()的位置上翻车,因为reversed()是作用在Comparator对象上的,不是作用在方法引用上的。
枚举类型也是这次的高频考点。选择题里给了一段枚举的代码,里面定义了构造器、字段和一个抽象方法,然后问你枚举的实例创建顺序是什么、switch匹配枚举时底层是什么类型。说实话,很多人把枚举当成“加了糖的常量类”,忽略了一个关键点:枚举是final的类,每个枚举常量都是该类的一个实例,而且是静态final的。所以枚举的构造器不是你想什么时候调就什么时候调的,它在类加载时就固定了。
还有标识符命名规则也来凑热闹,给了一堆字符串让你选哪些能当Java标识符。这种题一般不会故意坑人,但要注意关键字不能作为标识符、$和_可以出现在标识符中但有些位置不受推荐。我印象最深的是有个选项是_,在Java 9之前_是合法的标识符,但Java 9开始单独的下划线被禁止了。这种题纯粹考知识更新,如果你没跟进版本特性大概率会选错。
2.2 集合框架:HashMap和ArrayList是永远的神
集合这块,小满笔试考得很“主流”,但问题问得很刁。
先说ArrayList。问的是ArrayList初始容量是多少、什么时候扩容、扩容到原来的多少倍。如果你答上来“初始容量10、添加第11个元素时扩容、扩容为原来的1.5倍”,恭喜你只答对了一半。因为Java 8的ArrayList懒加载策略,实际上你new ArrayList()的时候,底层数组是空的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,只有第一次add时才初始化成容量10。这道题就把很多背答案的同学筛掉了。
HashMap更是重头戏。选择题里出现了很经典的“JDK 7和JDK 8中HashMap的区别”,问链表转红黑树的阈值是多少。答案是链表长度到8转红黑树、红黑树节点数降到6退化为链表。这里有个细节很多人不理解,为什么是8和6,而不是7和7?其实是为了避免在阈值附近反复振荡转换,浪费性能。别小看这个位运算级的细节,面试官是真会追着问的。
另外还考了ConcurrentHashMap,问的是JDK 8里它还分段锁吗。答案是已经不再用Segment分段锁了,而是改用CAS + synchronized锁住桶的头节点。如果只背过“分段锁”三个字,看到这题就懵了。
集合框架的学习建议:不要只背结论,要把源码下载下来读一遍关键方法。比如HashMap.putVal()的核心流程、ArrayList.grow()的扩容逻辑,读一遍源码比背十篇博客都管用。笔试里那些“题目代码问你输出”的题,往往就是从源码行为反推出来的。
2.3 并发编程:考的是并发三大特性的落地
并发这部分,小满笔试里的风格很“实操”——给一段多线程代码,让你判断哪些地方有线程安全问题。
具体来说,有一道题是这样的:一个类的成员变量是int count,两个线程各自循环10000次count++,问你最终值是多少。答案是“不确定,可能小于20000”。然后它问你如果想保证结果正确,可以用哪几种方式。选项里有volatile、synchronized、AtomicInteger、LongAdder等。
这里就藏着个经典误区:volatile能不能保证count++的原子性?不能。volatile保证的是可见性和有序性,但count++是“读-改-写”三步操作,不是原子的。所以选volatile就错了。正确选法是synchronized或AtomicInteger。这道题我印象很深,因为出题人把volatile藏在选项里,就是等着那些只背了“volatile保证可见性”就去答题的人往坑里跳。
线程池也考了,而且考得深。问ThreadPoolExecutor构造方法中各个参数的含义,以及任务提交后是怎么排队的。核心线程满了进阻塞队列,队列满了开非核心线程,线程数到最大值还有任务来就触发拒绝策略——这个流程你得烂熟于心。还额外问了一个有意思的点:Executors.newCachedThreadPool()的阻塞队列是什么类型,答案是SynchronousQueue,因为它是直接交接的,不缓存任务,这才会导致“来一个任务就开一个线程”的假象。
我自己的经验是,并发这块不要光背“三特性”“线程状态”,而是要自己写一个小Demo,用jstack看线程状态,用jvisualvm监控锁竞争。当你亲眼看到死锁、看到线程阻塞,这些知识点才会真正和你融为一体。
2.4 JVM与内存:出现了OutOfMemoryError,你该怎么查
今年好多热词里都出现了java: outofmemoryerror: insufficient memory,说实话这行报错太有代表性了,凡是搞Java的几乎都见过。小满笔试JVM部分的题,有一半就是围绕这类问题出的大题。
我记得简答题里有一道是:线上服务出现OutOfMemoryError,你会怎么排查。这题看似是开放题,实际上考察的是完整的排查思路。你得回答出:先用jstat看堆内存和GC情况,再用jmap -dump导出堆转储文件,接着用MAT或VisualVM分析大对象和引用链,最后定位到具体代码行。如果堆内存正常但一直OOM,还要考虑是不是线程创建过多、是不是堆外内存(Direct Memory)溢出了。这道题能拿全分的人,得有真实的线上排障经验,不然就只能写出一句“加大堆内存”的废话。
另一道选择题也很有意思,问你创建对象时,内存分配优先在Eden区还是老年代,什么情况下对象会直接进入老年代。这里有个隐藏考点:大对象(比如一个很大的数组)会直接分配到老年代,避免在新生代里反复复制。还有长期存活的对象,年龄达到15岁(CMS默认是6岁)会晋升到老年代。但如果你开了-XX:+UseTLAB,对象会优先在TLAB上分配,这个细节也是要命的。
JVM这部分的备考,我强烈建议大家亲手把JDK自带的jcmd、jstat、jmap这些工具在本地跑一遍。没有实操经验的话,你背再多“新生代、老年代”的概念,碰到稍微灵活的题目还是会露馅。
2.5 Spring与MySQL:框架原理也不能只停在“会用”
小满笔试对Spring的考察,侧重在“原理”二字。有一道选择题问Spring Bean默认的作用域是什么,这题很简单,选singleton。但下一道题就难度陡增:多个单例Bean之间互相依赖,会不会产生循环依赖问题?Spring能不能解决构造器注入的循环依赖?答案是:Spring能解决setter注入和字段注入的循环依赖(通过三级缓存),但解决不了构造器注入的循环依赖,因为构造器在实例化阶段就必须完成依赖注入,这时三级缓存还没法提供半成品对象。这个考点在真实面试里也是高频题,笔试里遇到说明出题人确实懂行。
MySQL部分,索引和事务是绝对的主角。题目问你InnoDB的索引结构是什么,答案是B+树而不是B树,然后问为什么用B+树。这里要注意答全:B+树所有数据都存在叶子节点,非叶子节点只存索引值,因此单页能容纳更多索引项,树更矮,IO次数更少;叶子节点之间有指针相连,方便范围查询。光答“非叶子节点不存数据”还不够,得把范围查询的优势也说出来。
事务隔离级别也考了,四个级别从低到高是读未提交、读已提交、可重复读、串行化,然后重点问InnoDB默认的隔离级别是什么,答案是可重复读,这个大家基本都知道。但深入一问:可重复读怎么解决幻读的?答案是通过MVCC + Next-Key Lock(间隙锁+记录锁的组合)。如果只背了“MVCC能解决幻读”这种简化的结论,一旦被追问“MVCC只能解决快照读的幻读,当前读的幻读要靠Next-Key Lock”,就很容易卡壳。
2.6 编程题:快速排序、冒泡排序,还有一道链表题
这次笔试的两道编程题,第一道是“排序题”:给定一个数组,要求用快速排序实现升序排列。这题看起来很基础,但面试官在笔试环境里考快排是有用意的。很多人排序只会调用Arrays.sort(),真让你手写快排就抓瞎。我在这里把快排的关键点说一下:
public void quickSort(int[] arr, int left, int right) { if (left >= right) { return; } int pivot = partition(arr, left, right); quickSort(arr, left, pivot - 1); quickSort(arr, pivot + 1, right); } private int partition(int[] arr, int left, int right) { int pivotValue = arr[right]; int i = left - 1; for (int j = left; j < right; j++) { if (arr[j] <= pivotValue) { i++; swap(arr, i, j); } } swap(arr, i + 1, right); return i + 1; }这里用的挖坑法或者双指针法都行,关键是要能写出稳定的partition逻辑,并说清楚时间复杂度最坏情况是什么、怎么避免。快排最坏情况下是O(n^2),比如数组本身有序而你每次都取最后一个元素当pivot,这时候就得考虑随机化pivot或者三数取中法。
第二道编程题是单链表反转,要求用递归和迭代两种方式实现。这两种方法都要会,而且笔试环境没有IDE提示,得靠手感和肌肉记忆写出来。迭代法用一个prev指针一路往后指,递归法思路是“先反转后面所有节点,再让当前节点的下一个节点指向自己”。如果对链表不熟,很容易在递归的终止条件上写错,出现空指针或者环。
关于冒泡排序,虽然这次编程题没直接考,但选择题里出现了“冒泡排序最优时间复杂度是多少”这种题。答案是O(n),前提是在内层循环里加了“本轮是否发生交换”的标志位,如果没有交换说明已经有序,直接跳出。注意,如果没加任何优化,冒泡排序的最好时间复杂度也是O(n^2)。这个细节也提醒我们:基础排序算法不要背一个版本就完事,要理解每种优化的动机。
3. 实操过程与答题策略
3.1 考前准备:把这些环境坑提前踩平
笔试是线上进行的,所以环境问题很要命。我考前在本地把JAVA_HOME、PATH都重新配了一遍,顺手确认了java -version和javac -version的输出,因为如果你本机装了多个JDK版本,编译版本不一致很容易出现“源发行版17需要目标发行版17”这种报错。
这里展开说一下环境变量配置:Windows下就是新建JAVA_HOME指到JDK安装目录,然后在PATH里加上%JAVA_HOME%\bin。但要注意,如果你在PATH里同时配了多个JDK的路径,系统会按顺序找到第一个java.exe,所以可能你敲java -version是17,而IDE里配置的运行环境却是8,这种不一致会引发一堆莫名其妙的编译错误。
还有一件特别容易坑人的事:Lombok报错。很多同学本机装了Lombok插件,但线上笔试环境是网页编辑器,没有插件处理注解,结果代码里用了@Data、@Slf4j这类注解,一编译就报类似“you aren't using a compiler supported by lombok, so lombok will not work”的错。遇到这种情况别慌,笔试环境不支持Lombok是常态,尽量不要在提交的代码里依赖Lombok生成的方法,手写getter/setter或者换个实现方式,稳一点。
另外,线上编译器默认编码不一定是UTF-8,如果题目的示例代码里带了中文注释,你本地跑没问题,一提交到平台就可能乱码。这种情况可以在代码文件头加上// -*- coding: utf-8 -*-,或者确保编辑器右下角编码格式是UTF-8。VSCode用户还可以在设置里搜files.encoding,把它固定成UTF-8,能少很多乱码烦恼。
3.2 时间分配与答题顺序
120分钟,我的实际分配是这样的:选择题30分钟,填空题15分钟,简答题35分钟,编程题30分钟,剩下10分钟检查。听起来挺紧凑,实际操作中,选择题里那些代码阅读题特别耗时间,一道题可能要3到4分钟。
我的建议是先做简答题和编程题,原因很简单:这些题目分值大、按点给分,就算你只能写出思路也能拿部分分。如果你先花大把时间做选择题,做到最后编程题只剩15分钟,那就很被动了。选择题哪怕蒙一下,还有25%的正确率兜底,简答题和编程题蒙不了,靠的是实打实的输出。
编程题写完后,一定要留时间做“边界测试”。比如给空数组、单元素数组、已经有序的数组分别跑一遍。很多线上笔试平台不会告诉你哪个测试用例没过,所以你必须自己把常见边界情况都列出来,逐项验证。
3.3 代码答题时的输出规范
笔试机考的评判标准通常是自动跑测试用例,对输出格式极其敏感。我这次第一道排序题要求输入一行整数,输出升序排列的整数,用空格分隔。如果你最后多打了一个空格,或者用了换行结尾,某些平台会判“格式错误”。
我的习惯是:不要在System.out.println()里做复杂的字符串拼接,而是用StringJoiner或者StringBuilder组装好再一次性输出。这样既避免了多次IO的性能浪费,也容易控制最后一个元素后面没有多余空格。比如:
StringBuilder sb = new StringBuilder(); for (int i = 0; i < arr.length; i++) { if (i > 0) { sb.append(" "); } sb.append(arr[i]); } System.out.println(sb.toString());还有一个细节:如果题目让你输出的是浮点数,注意保留几位小数。不同平台的精度要求不一样,我一般是先在注释里声明// 输出保留两位小数,然后统一用String.format("%.2f", value),避免因精度问题被扣分。
3.4 简答题怎么写才能拿高分
简答题是很多人容易忽视的失分项。小满笔试的简答题不是让你写长篇大论,而是按要点给分,所以你写的时候一定要分条、清晰。比如“说说HashMap的put流程”,你最好这样组织:
- 计算key的hash值,通过
(n - 1) & hash定位到桶的位置; - 如果桶为空,直接new Node放入;
- 如果桶不为空,比较链表头节点的hash和key是否相等,相等则覆盖;
- 如果不相等,遍历链表,找到末节点追加,如果链表长度超过8则转红黑树;
- 插入完成后检查size是否超过threshold,超过则扩容。
这种答题方式,阅卷人一眼就能看到得分点,比写成一段话更容易满分。还有一个技巧是:遇到“会不会、能不能”这类判断题,第一句先给出结论。比如“Spring能不能解决构造器循环依赖?”先答“不能”,再展开解释。很多阅卷人看的就是那个关键字,你放在句首就不用担心他看不见。
4. 笔试题外的话:面试与后续准备
4.1 笔试后面试官可能会追问什么
笔试结束不代表战斗结束,小满的流程通常是笔试通过后紧接着安排技术面。面试官手里是有你笔试答卷的,所以他大概率会拿着你做错的题或者写得含糊的答案追问。
这里有个很实用的准备思路:笔试结束后,立刻把每道题的答案重新过一遍,尤其是那些“蒙对了”的题。因为面试官如果要问,一定是从这些题目延伸出去问“为什么”。比如选择题里你选了ConcurrentHashMap在JDK 8用的是CAS + synchronized,面试官就可能追问synchronized锁的是对象还是类、锁升级的过程是什么。这些内容如果笔试后没有趁热打铁梳理一遍,到面试时就很难答得流畅。
4.2 常见报错与问题速查表
我把自己这段时间遇到的、以及跟同学交流收集到的典型问题整理成了表,笔试踩坑的人可以参考:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
源发行版 17 需要目标发行版 17 | 项目JDK版本与IDE编译级别不一致 | 检查Project Structure和Maven的java.version,统一JDK版本 |
OutOfMemoryError: Insufficient memory | 堆内存不足或创建的线程过多 | 用jmap导出堆快照分析,同时检查系统可用内存 |
| Lombok相关编译报错 | 缺少注解处理器或编译器不兼容 | 确保IDE安装Lombok插件且开启Annotation Processing |
| 控制台中文乱码 | 编码不统一 | 统一文件编码为UTF-8,设置控制台编码-Dfile.encoding=UTF-8 |
ArrayIndexOutOfBoundsException | 访问了不存在的数组下标 | 检查循环边界,Java数组下标从0开始,最大为length-1 |
NullPointerException | 对null对象调用了方法 | 使用Optional或提前判空,查看异常堆栈定位行号 |
| 代码在本地能跑平台报错 | 平台JDK版本或依赖不同 | 尽量只用JDK原生API,避免使用第三方库 |
4.3 心态和后续学习路线
最后说点掏心窝子的话。笔试这东西,准备得再充分也可能遇到知识盲区。我这次有一道关于Comparator.comparing链式调用的选择题就是凭感觉蒙的,好在整体没有崩盘。如果你某道题真不会,用排除法把明显错的选项划掉,把答案锁定在两个选项之间后凭直觉选一个,然后把更多时间留给后面能拿分的大题。
笔试踩坑之后,我给自己规划了三个阶段的学习路线。第一阶段重新精读集合、并发、JVM三块源码,动手跑一遍JDK自带工具;第二阶段把Spring Boot自动配置、Bean生命周期、MySQL索引下推这些原理吃透,写技术笔记;第三阶段保持每天手写一道算法题的节奏,重点练链表、二叉树、动态规划。这条路线也同步分享给正在备考的朋友,希望你能少踩我踩过的坑,一次上岸。