Java内推笔试复盘:从冒泡排序到JVM基础考点解析
对于Java工程师的内推笔试,很多人的第一反应是去翻面经、背题,但真正参加过的人会告诉你,笔试和面试是两个完全不同的游戏。
2016年360的内推笔试题放到今天来看,技术栈和框架层面已经发生过好几轮更替,但基础题型的考察逻辑几乎没有变化。围绕JAVA面试题、JAVA八股文、JAVA基础、冒泡排序JAVA、快速排序JAVA实现这些高频热搜词,我结合当年那批题目的出题思路,做一个完整的技术复盘。这篇内容不只是对题,更想把每一类题背后的考察意图、失分点、以及限时状态下的答题策略讲清楚,适合正在准备Java笔试的应届生,也适合那些项目做了不少、但基础题容易翻车的在职候选人。
1. 内推笔试的考察边界:它到底在筛选什么人
先聊一个很多人忽略的问题:内推笔试和统考笔试、面试,考察导向是完全不同的。
360这类公司的内推,本质上是一种半定向的招聘通道。内推人已经帮你做了第一层背书,公司方的核心诉求不是再筛一遍"会不会背题",而是用一种低成本的方式验证三件事:你的基础功底是否扎实、你写代码是否严谨、你是否有基本的工程意识。所以2016年这套题的整体风格偏向基础题和中等题的组合,难题和偏题占比很小,但陷阱密度比校招统考更高。
现在网上一搜"JAVA笔试"全是各种高深框架题、中间件原理题,反而容易把方向带偏。实际上大厂内推笔试的命制逻辑往往是反过来的:先划定一个基础能力底线,再在这个底线上增加干扰项。比如排序算法,不会直接考你"快排的时间复杂度是多少"这种填空题,而是给你一段写了一半的代码,让你补全并指出边界条件。这种考察方式决定了一个事实:背结论没有用,必须真正理解代码的执行过程。
具体到科目配比,2016年前后Java研发岗的笔试大体遵循这样一个比例:
| 考察模块 | 大致占比 | 典型题型 |
|---|---|---|
| 算法与数据结构 | 30%-40% | 排序、链表、二叉树、动态规划 |
| Java语法与面向对象 | 20%-25% | 代码输出题、概念辨析题 |
| 集合框架与并发 | 15%-20% | HashMap原理、线程安全、锁 |
| JVM与内存管理 | 10%-15% | 内存区域、垃圾回收、OOM |
| 网络与操作系统 | 5%-10% | TCP、进程线程、IO模型 |
| 软件工程与设计 | 5%左右 | 设计模式、代码重构思路 |
这个配比到今天依然有参考价值。你去看那些"JAVA面试八股文"的目录,绕来绕去还是这些模块。这套题的核心难点不是单题难度,而是在有限时间内稳定输出基础能力。
2. 排序与算法题:从冒泡到快排的失分链
算法题在Java笔试中永远是重头戏,而排序又是算法题的常客。那套笔试题里的排序题目考察得很典型:一道冒泡排序的优化题,一道快速排序的实现题。两道题都不算难,但失分率非常高。
2.1 冒泡排序的优化写法:标志位只是入门
很多人在笔试里看到冒泡排序,直接默认是最基础的写法:
public void bubbleSort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } }这样写不算错,但放在内推笔试里,基本拿不到加分。当年的题目已经把基础版写出来了,要求是"指出该实现的问题并优化"。这里的问题有两层。
第一层是无效遍历问题。如果数组在某一轮遍历中已经完全有序,后续轮次还在继续执行,纯浪费。解决方法是引入一个布尔标志位:
public void bubbleSortOptimized(int[] arr) { boolean swapped; for (int i = 0; i < arr.length - 1; i++) { swapped = false; for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } if (!swapped) { break; } } }但如果你只答出这一层,仍然不够。第二层是有序区边界问题。传统写法里,内层循环的边界是arr.length - 1 - i,这个边界假设每一轮都会把当前最大值"冒"到最后,所以有序区每次加1。但真实情况下,某轮可能冒泡了多个元素,最后发生交换的位置可能远小于当前的边界。所以更优的写法是记录最后一次交换的索引:
public void bubbleSortBest(int[] arr) { int lastSwapIndex = arr.length - 1; while (lastSwapIndex > 0) { int currentSwapIndex = 0; for (int j = 0; j < lastSwapIndex; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; currentSwapIndex = j; } } lastSwapIndex = currentSwapIndex; } }这个版本通过lastSwapIndex记录本轮最后发生交换的位置,下一轮只需要遍历到该位置即可,因为其后的元素已经有序。笔试现场能写出这个版本的人并不多,但一旦写出来,面试官对你的评价会立刻不一样。
2.2 快速排序的边界处理:笔试里的翻车重灾区
快速排序那道题,题目直接给出了一个快排模板,要求补全partition方法。这是很典型的考察方式,因为快排的思想大家都会背,但手写时边界问题极其容易出错。
public int partition(int[] arr, int low, int high) { int pivot = arr[low]; while (low < high) { while (low < high && arr[high] >= pivot) { high--; } arr[low] = arr[high]; while (low < high && arr[low] <= pivot) { low++; } arr[high] = arr[low]; } arr[low] = pivot; return low; }这个版本的partition是教科书式的挖坑法,看起来很简洁,但笔试题里的陷阱往往藏在细节里。
最容易翻车的点有两个。第一个是内层循环的条件必须带等号。如果写成arr[high] > pivot,遇到与pivot相等的元素时会出现死循环,因为左右两个指针都无法越过相等元素。第二个是递归终止条件的边界,如果low >= high没有处理好,会出现无限递归或数组越界。
public void quickSort(int[] arr, int low, int high) { if (low >= high) { return; } int pivotIndex = partition(arr, low, high); quickSort(arr, low, pivotIndex - 1); quickSort(arr, pivotIndex + 1, high); }至于时间复杂度,很多人只记得快排平均是O(nlogn),但忽略了最坏情况是O(n^2),以及最坏情况发生在数组已经有序、且每次选到的pivot都是最大值或最小值时。笔试题如果继续追问"怎么优化",答案也很明确:三数取中法,或者随机选择pivot。
2.3 手写代码时的注意事项
笔试写算法题,尤其是手写代码的时候,有几个细节决定了你能拿多少分。
- 变量命名要能看懂,
arr、low、high、pivot这种是常规操作,但不要写a、b、c。 - 边界条件的判断必须在循环开头完成,不要在循环体中间才开始判断。
- 如果题目要求补齐
partition方法,不要在main方法里做了一堆测试后才返回完整代码,直接把核心方法写完整。 - 空间复杂度要写清楚,快排如果不算递归栈空间,是
O(1)的原地排序,算递归栈则是O(logn)平均空间。
算法题的值,在于写完后你是否有自我验证的过程。我记得当时很多人在partition里把等号写丢了,自己却完全没有意识到。所以笔试答完后留出2-3分钟,拿一组简单的测试用例(比如[5,1,4,2,8])在心里或草稿纸上走一遍,能救回不少分。
3. Java基础题:表面考语法,实际考理解深度
Java基础在笔试里占了不小的比重,但这类题很少直接问"什么是多态",而是通过代码输出题和概念辨析题来考察。2016年那套题里有几道典型的代码输出题,放到现在的JAVA面试八股文里依然高频出现。
3.1 String、StringBuilder、StringBuffer三兄弟
一道很经典的题:给出下面代码,写出输出结果。
String s1 = "hello"; String s2 = "hello"; String s3 = new String("hello"); System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1.equals(s3));输出结果是true、false、true。这个结论大多数人都背过,但笔试一般会在这个基础上继续加码。
进阶版的长这样:
String s1 = "hello"; String s2 = "he" + "llo"; String s3 = "he"; String s4 = s3 + "llo"; System.out.println(s1 == s2); System.out.println(s1 == s4);"he" + "llo"属于编译期常量折叠,会在编译时直接拼接成"hello",所以s1 == s2是true。而s3 + "llo"涉及到变量拼接,编译期无法确定值,运行时实际上是创建了一个新的StringBuilder来拼接,最终得到一个新的字符串对象,所以s1 == s4是false。
这里能区分出真正理解和不理解的人。笔试现场如果只是背了"常量池"三个字,遇到变量拼接就容易翻车。
再往下追问,就到了StringBuilder和StringBuffer的区别。线程安全性是核心区分点:StringBuffer的方法加了synchronized,线程安全但性能略低;StringBuilder是非线程安全的,性能更高。在实际开发中,方法内部的局部字符串拼接用StringBuilder就够了,涉及跨线程共享才需要StringBuffer。
3.2 equals与hashCode的契约关系
这几乎是必考题。笔试常见的问法是:重写equals时为什么必须重写hashCode?
标准答案是:两个对象如果equals相等,那么它们的hashCode必须相等。如果违反了这条契约,在HashMap、HashSet等基于哈希的集合中会出现严重的逻辑错误:对象存入HashSet后,再找一个equals相等的对象去查询,落到了不同的哈希桶里,导致查不到。
当年那道题的坑在于,题目给了一个自定义类,只重写了equals没有重写hashCode,然后问HashSet的大小是多少。如果你能写清楚"导致两个相等的对象被放进了不同的桶,集合里出现了两个逻辑上相等的元素"这种话,说明你理解的不只是语法,还有集合框架的原理。
3.3 final关键字的真实语义
final这个关键字,笔试很少直接考定义,而是喜欢用代码来混淆。比如:
final List<String> list = new ArrayList<>(); list.add("hello");问这段代码能不能编译。答案是可以,因为final修饰的是引用,不能改变的是指向哪个对象,而不是对象内部的状态。list.add("hello")修改的是对象内部的数据,完全合法。
这个概念的干扰性很强,因为初学者常常把final和"不可变"画等号。这道题的实际意义在于,它考察了你是否具备区分"引用不可变"和"对象不可变"的能力,而这种能力在写并发代码时非常重要。比如用final修饰一个List并跨线程传递,你只保证了引用不变,如果另一个线程改了这个List的内容,你面对的仍然是线程安全问题。
3.4 继承、多态与初始化顺序
考察继承的代码输出题也是重头戏,而且几乎100%会考到初始化顺序。经典题目长这样:
class Parent { static { System.out.print("A"); } { System.out.print("B"); } Parent() { System.out.print("C"); } } class Child extends Parent { static { System.out.print("D"); } { System.out.print("E"); } Child() { System.out.print("F"); } } public class Main { public static void main(String[] args) { new Child(); } }输出顺序是ADBCEF。知识点拆开来看:
- 类加载阶段执行静态代码块,先父类后子类,所以是
A然后D。 - 实例化阶段,先执行父类的实例代码块,再执行父类构造器,所以是
B然后C。 - 最后执行子类的实例代码块和构造器,所以是
E然后F。
这道题的失分点在于,很多人把"实例代码块"和"构造器"的先后顺序搞错了。实例代码块在构造器之前执行,这是Java语法层面的规定,记忆方法是:构造器可以看成一段特殊的方法,而实例代码块相当于嵌在最前面的通用初始化逻辑。
4. 集合与并发:笔试中的高频失分区
集合框架和并发是Java笔试里最容易拉开分数差距的模块。这个部分的特点是:概念题多、代码题少,但对"理解深度"的要求非常高。如果你只是背过HashMap"底层是数组加链表"这种结论,一旦题目换成场景分析,很容易露馅。
4.1 HashMap的底层机制与JDK版本差异
2016年那场笔试,HashMap还在JDK 7/8过渡的时代。现在JAVA面试题里关于HashMap的内容已经非常卷了,但核心还是那几个:哈希算法、哈希碰撞、扩容机制。
笔试题常见的问法:
- HashMap底层结构是什么?答数组加链表,JDK 8之后链表长度超过8且数组长度超过64时转红黑树。
- 为什么用红黑树而不用二叉搜索树?因为二叉搜索树在极端情况下会退化成链表,时间复杂度从
O(logn)退化为O(n),红黑树能保证最坏情况下的时间复杂度,同时维护成本比AVL树低。 put方法的完整流程是什么?先根据key的hashCode计算哈希,再通过(n - 1) & hash定位到桶的位置。如果桶为空直接插入,否则遍历链表或树,找到相同key则覆盖,找不到则尾部插入。
当年的题目没有问这么深,但有一道很有代表性的题:给定一个自定义对象作为key,要求说明哪些行为会导致HashMap无法正常工作。
答案其实指向两个点。第一,如果这个类没有重写hashCode,那么默认的哈希值基于对象的内存地址,不同的对象即使逻辑上相等,哈希值也不同。第二,如果重写了hashCode但没重写equals,会导致哈希值相同但equals不等,查询时拿新对象去匹配不到原有的键值。把这两点说清楚,比把整个put流程背一遍更让阅卷人认可。
4.2 ArrayList不等于线程安全
集合框架里还有一个高频陷阱题,就是ArrayList的线程安全性。很多人知道Vector是线程安全的,ArrayList不是,但追问"为什么"或者"具体怎么出问题"时反而回答不上来。
考察方式是:两个线程同时向一个ArrayList添加元素,会发生什么?
- 可能抛出
ArrayIndexOutOfBoundsException。ArrayList的add方法分两步:先检查容量ensureCapacityInternal,再执行elementData[size++] = e。两个线程同时检查容量时都通过,但其中一个已经添加了元素,另一个继续用旧索引写入,就会越界。 - 可能元素数量不对。两个线程同时执行
size++,由于size++不是原子操作,最终size可能只增加了1,而不是2。
这种分析方式的价值在于,它不考你是否背过"ArrayList是线程不安全的"这句话,而是考你能不能从代码执行的粒度解释这种不安全。有了这个分析习惯,后续学CopyOnWriteArrayList的写时复制机制、ConcurrentLinkedQueue的CAS操作,都会轻松很多。
4.3 volatile与synchronized:并发题里绕不开的基石
笔试中的并发题很少直接考JUC的高级工具,而更倾向于考察volatile和synchronized这类基础原语。
一个经典问法:volatile能保证原子性吗?答案是不能。volatile只保证了可见性和有序性,但不保证原子性。比如count++这种操作,即使声明为volatile,两个线程同时执行时仍然可能丢失更新。因为count++实际上分解为读取、加1、写回三个步骤,volatile只保证了每个步骤的可见性,无法保证三步作为一个整体不被中断。
那笔试里为什么喜欢考这个?因为很多人会把"可见性"和"原子性"混为一谈。如果你能答出"volatile解决的是线程缓存不一致的问题,synchronized解决的是多个线程同时修改同一份数据的问题",就已经踩中了出题人的期望。
另一个高频点是被synchronized修饰的静态方法和实例方法的区别。静态方法锁的是Class对象,实例方法锁的是当前实例this。这意味着,一个线程执行静态同步方法时,不会影响另一个线程执行同一个类的实例同步方法。这个点常被用来出一道"判断是否互斥"的代码题,如果对这个区别没有清晰的认识,很容易写错答案。
4.4 并发编程的延伸:理论到实战的过渡
基础题做完之后,笔试偶尔会加一道综合题,把并发引到实际场景里。这种题不会直接问"线程池有哪几种拒绝策略",而是给出一个场景,要求设计方案。
比如:高并发场景下要统计一个接口的调用量,用什么方案?答案可以从AtomicLong的CAS操作说起,说明它比synchronized加锁吞吐更高;如果要求更高性能,还可以用LongAdder分段累加的思路。
这类题考察的是你在实际项目中是否真正用过并发知识,而不是停留在背概念。我在实际开发中的体会是,笔试阶段如果能在这类综合题上写出两个层次:先提基础方案,再分析瓶颈并给出优化方案,整个试卷的区分度就出来了。
5. JVM内存与异常处理:一眼看穿内功深浅
很多人以为JVM和异常处理是笔试里的"冷门模块",但实际上,2016年那套题里这两块的分值并不低。原因也很现实:java: outofmemoryerror: insufficient memory这种报错在开发中太常见了,与其说是笔试题,不如说是工作场景的提前预习。
5.1 内存区域划分与发生位置
JVM内存区域的题目几乎没有缺席过。考察方式通常是画出内存区域图,或者给出一个场景问你"哪个区域会报OOM"。
Java运行时数据区可以分成以下几块:
| 内存区域 | 线程共享 | 存储内容 | 常见异常 |
|---|---|---|---|
| 堆 | 共享 | 对象实例、数组 | OutOfMemoryError |
| 方法区 | 共享 | 类信息、常量、静态变量 | OutOfMemoryError |
| 虚拟机栈 | 私有 | 局部变量表、操作数栈 | StackOverflowError |
| 本地方法栈 | 私有 | native方法调用 | StackOverflowError |
| 程序计数器 | 私有 | 当前线程执行的字节码行号 | 无 |
这道题表面上是在考划分,实际上在考你是否理解"哪些内存是线程共享的,哪些是私有的"。因为私有的内存区域随线程创建和销毁,不会出现多个线程同时往里写数据的问题;而共享区域(堆、方法区)才会面临并发访问和内存溢出的风险。
5.2 OOM的种类与排查思路
java: outofmemoryerror: insufficient memory这个报错信息,热搜里出现频率很高,说明大家在开发中经常撞上。笔试里如果出OOM相关题,一般不会只让你背定义,而是给一段代码,问你"这段代码执行后会怎样"。
典型的堆内存溢出场景:
List<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); }这会导致堆内存溢出,报java.lang.OutOfMemoryError: Java heap space。常见的排查思路是:先用jmap -dump:format=b,file=heap.bin <pid>导出堆快照,再用jhat或者VisualVM分析大对象和引用链,定位到内存泄漏的根源。
如果是栈溢出场景,比如无限递归,报的是java.lang.StackOverflowError。这里容易混淆的一点是:栈溢出不叫OOM,它是因为栈深度超过了虚拟机允许的最大深度。笔试题目如果挖坑,会故意把这两种异常混在一起。
另外一个高频点是数组越界异常,也就是ArrayIndexOutOfBoundsException。这个东西看着基础,但笔试里喜欢把它跟for循环的边界条件放在一起考。比如:
int[] arr = new int[5]; for (int i = 0; i <= arr.length; i++) { arr[i] = i; }这段代码在i = 5时就会触发ArrayIndexOutOfBoundsException。笔试里如果附带调试题,这就是一个典型的定位训练点。你会不会从报错信息里的行号快速定位到循环条件,而不是一脸懵地看整个文件。
5.3 异常处理的5个基础考点
异常处理在笔试中的考察相对固定,无外乎这五种:
RuntimeException与CheckedException的区别。throw和throws的用法。try-catch-finally中finally的执行顺序。- 一个方法中如果既有
return又有finally,最终返回什么值。 - 自定义异常的写法。
第四个考点非常经典,也是容易失分的点。下面这段代码,返回值是什么?
public int test() { int a = 1; try { a = 2; return a; } finally { a = 3; } }答案是2。因为finally在return之后执行,return a会先把a的值复制到一个临时存储区,然后执行finally,最后将临时存储区的值返回。所以finally中修改了a,但不影响返回值。这个知识点在实际项目中不太常用,但笔试里考得非常多,属于典型的八股文考点。
如果题目改成finally中也有return:
public int test() { int a = 1; try { a = 2; return a; } finally { a = 3; return a; } }那么返回值就是3,因为finally中的return会覆盖try中的return。这两种情况对比记忆,就不容易搞混。
5.4 类加载与双亲委派
除了内存,JVM题里偶尔会出现类加载机制,比如双亲委派模型。这个概念在JAVA基础面试题里的出现率相当高,核心是:一个类加载器收到类加载请求后,不会先自己尝试加载,而是先委托给父类加载器,父类加载器无法加载时才下传给子类。这样设计的目的是保证Java核心类库的安全性,比如java.lang.String永远由启动类加载器加载,不会被篡改。
笔试里如果考这个知识点,一般不会让你画类加载器的层次关系图,而是给一个场景判断。比如:如果你自己写了一个java.lang.String类放到classpath里,应用启动时会加载哪个String?答案永远是启动类加载器加载的JDK自带版本,因为双亲委派模型在父加载器能加载时根本不会轮到应用类加载器。能答出这个结论,说明你理解了模型的实际意义,而不是只记住了五个类加载器的名字。
6. 设计模式与工程意识:笔试阶段就能拉开差距的软实力
2016年那套题里,设计模式相关的题不多,但出现了一道让很多人头疼的场景设计题。这类题不算纯理论,更像是"软件工程题",考察的是你在实际开发中的抽象能力。
6.1 单例模式:为什么饿汉式常被诟病
单例模式是设计模式中的熟面孔,笔试里最常见的考察方式是手写一个线程安全的单例。大多数人能写出双重检查锁(Double-Checked Locking)版本:
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }这里volatile是必须的,因为new Singleton()这一步包含三个步骤:分配内存、初始化对象、将引用指向内存。如果不加volatile,指令重排可能导致另一个线程拿到一个尚未初始化完成的对象引用。
但笔试如果继续追问饿汉式的缺点,很多人就卡住了。饿汉式在类加载时就完成了实例化,如果这个单例对象的构造过程很重,而应用根本没用到它,就会造成资源浪费。更重要的是,饿汉式的实例化时机不受控制,在某些需要延迟加载的场景下并不合适。
6.2 观察者模式与解耦
除了单例,观察者模式出现的频率也比较高。笔试题目可能会给一个场景:一个订单状态发生变化时,需要同时通知物流系统、库存系统和积分系统,怎么做?
最差的答案是写一个update方法,里面挨个调用三个系统的接口。一旦后续新增一个通知目标,就必须修改update方法,违反了开闭原则。而观察者模式的思路是把订单作为被观察者,物流、库存、积分作为观察者,订单状态变化时统一通知所有观察者。
如果笔试里能写出一小段观察者模式的示意代码,比如定义一个OrderStatusSubject接口,维护一个观察者列表,notifyObservers方法遍历通知,就能在众多只写概念答案的人中脱颖而出。这个技能树对于后续阅读Spring的事件监听机制(ApplicationEvent与ApplicationListener)也很有帮助。
6.3 设计模式之外:笔试题里的工程思维
设计模式的题目其实只是在测试一种能力:你是否具备在变化中保持代码稳定的能力。真正的工程意识,在笔试题里还会以另一种形态出现——代码审查题。
题目会给一段读起来非常费劲的代码,比如一个方法有十几个参数、循环里有三层if-else嵌套、魔法数字满天飞。然后问你能提出哪些改进方案。这类题没有绝对的标准答案,但回答时如果能抓住以下几点,得分会明显更高:
- 将重复代码抽取成独立方法。
- 将魔法数字替换为常量或枚举类型。
- 用分支策略模式替代多层条件判断。
- 确认方法是否遵循单一职责原则。
这些点不需要你背设计模式的二十三种分类,但需要你在实际项目中真正被烂代码折磨过。很多应届生在这道题上半天憋不出一句话,就是因为他们的日常练习大多停留在"写一个能跑通的算法",缺乏"审查自己代码质量"的习惯。建议备考阶段养成一个习惯:每写完一段逻辑,回头看一眼,能不能减少一个参数、合并一个方法、给变量起一个更精确的名字。这个习惯带来的收益,会在笔试和入职后的代码评审中持续显现。
7. 限时答题的策略与典型失分点复盘
笔试考察的不只是知识储备,还有时间分配和心态管理。很多人不是不会做,而是做不完,或者在前面某道题卡了太久导致整场崩盘。
7.1 答题顺序的优先级
我见过太多人从第一题做到最后一题,结果在选择题上消耗了过多时间。对于Java研发岗的笔试,我的建议是:
- 先做代码输出题和概念辨析题。这类题耗时短,读完题基本就能出答案,能快速赚到基础分。
- 再做数组、字符串、排序类的算法题。这类题思路清晰,花几分钟写出来,稳赚不赔。
- 中间穿插做集合、并发、JVM的简答题。这些题需要组织语言,但不需要长篇大论,点到核心给分点即可。
- 最后集中攻克动态规划或复杂数据结构题。这类题一旦卡壳,立即先标记跳过,不要恋战。
这样的顺序能确保你在有限时间内先拿到"稳定分",再把剩余精力投入到"拉分题"上。
7.2 代码题的常见失分点
代码题是笔试中失分最严重的题型,我梳理了几个高频失分点:
- 边界条件没处理。循环的起止索引、空数组、单个元素,这些情况在笔试中占比很高,写完后一定要验证边界。
- 没有考虑输入为空的情况。比如快排传入长度为0的数组,
low=0, high=-1,如果没有提前判断low >= high直接递归,就会栈溢出。 - 变量命名混乱。你写的代码阅卷人要花30秒才能看懂,跟一看就懂的代码,得分差距是客观存在的。
- 没有写出必要的注释。笔试不是OJ,不需要每一行都加注释,但核心算法步骤加一行注释,能显著提升阅卷体验。
7.3 备考路线的重新梳理
围绕JAVA学习路线来准备笔试,我的经验可以浓缩成三个阶段。
第一阶段是打基础,重点是Java基础语法、面向对象、集合框架、异常处理。对应的热搜词就是"JAVA基础"、"JAVA面试题"、"JAVA面试八股文"这一层。这个阶段的目标是看到任何一道基础题,都能在3秒内定位到考察的知识点。
第二阶段是补深度,重点是JVM内存模型、并发编程、HashMap原理、类加载机制。对应JAVA八股文里最常被追问的那一批问题。这个阶段要追求"知其所以然",哪怕只是一个小概念,也要能讲出它在实际场景中的表现。
第三阶段是刷真题。找一套大厂近几年Java笔试真题,限时90分钟模拟。重点不是做对多少,而是检验自己在时间压力下的稳定表现。这一轮你会发现自己平时会做的题也可能出错,因为状态完全不同。
7.4 笔试现场的心态管理
最后聊一个可能很多人觉得虚、但实际上决定结果的事:心态。
笔试最容易出问题的时间点是开考后15到30分钟。这时候如果连续碰到两三道不太确定的题,很容易产生自我怀疑,然后反复改答案,浪费大量时间。我的建议是:每道题给自己设一个时间上限,选择题不超过2分钟,简单算法题不超过10分钟,复杂算法题不超过20分钟。到了时间就跳,全部做完再回头看。因为笔试的计分逻辑是"多得分者胜",而不是"题题做出者胜"。
还有一个很实用的技巧:对于概念题,能从多个角度写就不要只写一句干巴巴的定义。毕竟阅卷人看到的不只是你懂不懂,更是你组织技术语言的能力。比如问"什么是多态",只写"同一个方法在不同对象上有不同表现"勉强算对,但如果再写清楚重载是编译期多态、重写是运行期多态,并附一个场景说明,这份答案就明显更有竞争力。这个习惯在面试中的价值更大,因为面试官通常会根据你的回答往下追问,你能多铺一层,追问的空间就更可控。
8. 写在最后:一套题能复盘出的东西
把360这套2016年内推笔试题完整复盘一遍,你会发现,真正重要的不是具体题目,而是出题人试图验证的那几条底层能力:基础是否扎实、代码是否严谨、思维是否有深度、表达是否清晰。这些能力不分年代,也不会因为框架迭代而失效。
我在实际工作中带新人的时候有个体会:能把基础题讲清楚的人,上手项目通常也更快。因为基础题考察的是对语言和运行机制的理解,这种理解会体现在你写的每一行业务代码里:你会主动思考这个集合在并发场景下是否安全,你会留意字符串拼接在大循环里的性能损耗,你会习惯性地给关键方法加上边界检查。这些习惯都不是背题背出来的,而是在一次次基础训练中被磨出来的。
如果你正在准备Java笔试,我最后想分享的一个实操建议是:不要只搜"JAVA面试大全及答案"然后刷完就完事。挑3-5道经典题,把答案用笔写下来,再去对照标准答案,找出遗漏的点。这个过程比刷50道题更有用,因为它会逼你真正完成一次"知识提取"的内部训练。考场上你能调用的,永远不是你背过的东西,而是你真正理解的东西。
