1. 这份笔试题到底想筛什么样的人
货拉拉2018秋招Java工程师笔试题卷一(B),我看到这个标题的瞬间,脑子里闪过的第一个念头是:这又是一份典型的“基础为王”的校招卷子。虽然具体题目我没有逐字背下来,但这种规模的互联网公司校招笔试,考察逻辑其实高度一致——它不是在考你多会写框架,而是在筛掉那些“简历写得天花乱坠、一上手就露馅”的人。
先说说这份卷子对应的场景。货拉拉的业务核心是同城货运调度,技术栈以Java为主,涉及订单、调度、支付、消息推送这类高并发业务。2018年那会儿,货拉拉正处于业务扩张期,招Java工程师的重点是“能干活、基础扎实、可塑性强”。所以笔试题的定位非常清晰:Java基础占大头,算法题用来测思维,数据库和框架题用来筛项目造假。
如果你现在准备投这类公司的Java校招岗位,这份卷子的参考价值依然很高。原因很简单:Java面试题翻来覆去就是那些核心知识点——集合、并发、JVM、Spring、MySQL索引、排序算法。2018年的题目和2024年的题目,骨子里没有本质区别,变的只是包装方式。
这篇文章,我会从这份笔试题出发,把校招Java笔试常见的考点、每一类题目的应对思路、以及我在实际批改校招笔试卷子时看到的高频错误,完完整整拆给你看。不管你是即将参加秋招的应届生,还是想跳槽但基本功不牢的初级工程师,照着这份思路去复习,比盲目刷题效率高得多。
2. 整张卷子的考察维度拆解
2.1 四个必考的考察方向
把这份卷子和同期其他公司的Java笔试题放在一起看,你会发现在考察维度上是惊人一致的。原因很简单——笔试是成本最低的初筛手段,它必须在有限时间内覆盖最核心的基础能力,而不是考核具体的业务经验。
第一是语言基础。Java基础面试题里,考察点在几个地方非常固定:面向对象编程Java的三大特性、java运算符和表达式优先级、字符串常量池、异常体系、泛型擦除、反射原理。这些题没有太多技巧,就是看你有没有系统地读过Java基础,而不是只在培训班里敲过代码。
第二是集合框架。这个板块几乎是必考,而且占比相当重。HashMap在JDK 7和JDK 8之间的数据结构差异、扩容机制、为什么链表长度超过8才转红黑树、ConcurrentHashMap的分段锁和CAS机制、ArrayList和LinkedList在什么场景下选哪个。这类java面试题不是死记硬背就能答好的,你要能画得出数据结构的变化过程,才算真的理解。
第三是并发与JVM。2018年的卷子里,这部分一定有题,而且大概率偏向基础概念:JVM内存分区、堆和栈的区别、垃圾回收算法、什么情况下会触发java: outofmemoryerror: insufficient memory、synchronized和ReentrantLock的区别、volatile的可见性和禁止指令重排。这些题目在java面试八股文里都见过,但校招笔试和面试不一样,笔试考的是“背得准不准”,面试问的是“理解得透不透”。
第四是算法与数据结构。这部分的题型很固定:手写冒泡排序java实现、快速排序java实现、链表反转、判断回文串、二分查找、斐波那契数列。看起来简单,但笔试里坑很多,比如边界条件的处理、特殊输入的鲁棒性、时间复杂度分析。很多人在本地IDE里能跑通,一到笔试系统里就超时或者数组越界,就是这个原因。
2.2 从题型分布反推公司的用人偏好
这类卷子的题型分布通常是:单选或多选(20到30道)、简答/程序输出题(5到10道)、手写编程题(2到3道)。这是一个很合理的结构,单选考察知识覆盖面,输出题考察对语言细节的掌握程度,编程题考察动手能力。
我看过很多校招笔试卷子,也批改过不少,总结出一个规律:单选题里错的多的,通常不是知识点不会,而是概念混淆。举个例子,Integer的缓存范围是-128到127,==和equals的区别,字符串通过+拼接时内部是怎么处理的。这些细节在平时的项目里几乎用不到,但笔试试卷里频繁出现。为什么?因为公司要的是基础扎实的人,而不是只会调API的人。
从货拉拉这份卷子所处的时期来看,业务正在快速扩张,后端服务拆分的需求多,所以对并发基础、数据结构的要求会更高一些。简单来说就是:你可以暂时不懂分布式、不懂高并发架构,但Java语言本身的底子必须稳。
3. 核心考点精讲:Java基础与JVM陷阱
3.1 面向对象与运算符的八股陷阱
先说面向对象编程Java这个考点。大家都知道三大特性是封装、继承、多态,但笔试很少直接问定义,它喜欢考一些似是而非的说法。比如:“继承会破坏封装性,这句话对不对?”答案是“对”。因为子类继承父类后,可以直接访问父类的protected成员,这在一定程度上破坏了封装。这种题考的就是你对概念理解的深度,而不是背概念的速度。
还有java运算符和表达式这个点,别觉得简单,很多人在优先级上翻车。笔试常考的一道题是:
int a = 5; int b = a++ + ++a; System.out.println(b);答案是12。过程是:a++先返回5,然后a变成6;++a先把a变成7,再返回7;5+7=12。这类题考的无非是“++在前先加后取,++在后先取后加”,但放到表达式里,很多人一紧张就绕晕了。我的建议是:遇到这类题,不要心算,直接在草稿纸上画出每一步的值。
还有一个高频坑是字符串。字符串常量池、new String("abc")创建了几个对象、String a = "a" + "b"和String b = new String("ab")的区别。这些题在java基础面试题里属于送分题,但送分的前提是你真的去研究过Class文件里关于字符串字面量的存储方式。
3.2 JVM内存与OOM问题深度解析
搜索热词里出现了java: outofmemoryerror: insufficient memory,这个词一看就是某个运行环境报的错。但从笔试的角度看,JVM相关考点的核心在于:你得清楚OOM发生在哪块内存区域,什么场景下触发,以及怎么排查。
JVM内存分为堆、虚拟机栈、本地方法栈、方法区(元空间)、程序计数器。笔试常考点是:
- 堆OOM:
java.lang.OutOfMemoryError: Java heap space。常见原因是对象太多了,或者有对象一直在被引用无法回收。排查思路是jmap导出堆转储快照,用MAT分析大对象。 - 栈溢出:
java.lang.StackOverflowError。递归没有出口,或者方法调用层级太深。 - 方法区OOM:在JDK 8之后对应Metaspace,常见原因是动态生成类太多,比如大量使用CGLib代理。
在笔试里可能不会考具体命令,但会考“判断下面哪些情况会导致OOM”这类多选题。这里我总结一个判别模板:创建大量线程导致无法分配新的栈内存、大对象直接进入老年代且无法回收、字符串常量池溢出,这些都是OOM的典型场景。
这里还要提一个笔试高频考点:垃圾回收算法。标记-清除、标记-复制、标记-整理、分代收集,每一种的优劣对比都要能写出来。同时CMS、G1、ZGC这些收集器的适用场景也是常客。我批改过不少卷子,很多人写得出G1的特点,但说不清楚G1的“可预测停顿”是怎么实现的——答案在Region划分和优先回收垃圾最多的Region这个机制上。
3.3 并发编程:volatile与锁机制的关键细节
并发题在Java笔试里是“高分题”,因为能拉开差距。考察焦点集中在三个方向:synchronized和ReentrantLock的对比、volatile的语义、ThreadLocal的原理和内存泄漏风险。
先说volatile。它保证可见性和禁止指令重排,但不保证原子性。笔试里很经典的题是:
public class Test { volatile int count = 0; public void add() { count++; } public static void main(String[] args) { // 多个线程同时调用add(),count最终值是多少? } }答案是不确定,因为count++是“读-改-写”三步,不是原子操作。很多人知道volatile不保证原子性,但真正在题目里判断时还是容易想当然。你只需要记住一个简单结论:volatile适合一写多读的场景,不适合多线程写共享变量。
再说synchronized和ReentrantLock。两者都是可重入锁,但ReentrantLock多了可中断、可超时、公平锁非公平锁可选这些能力,并且必须手动解锁。笔试喜欢考“打印输出顺序”这类题,比如:
Thread t1 = new Thread(() -> { synchronized (lock) { System.out.println("A"); lock.wait(); System.out.println("B"); } });这类题的考点是wait会释放锁,而sleep不会释放锁。如果把sleep换成wait,输出顺序完全不一样。说白了,并发题就是考“锁的释放时机”和“线程状态切换”,把这些关键点死磕清楚,大部分并发题都能应对。
4. 集合框架与数据库高频题精讲
4.1 HashMap与ConcurrentHashMap的底层对比
集合框架这块,HashMap是绝对的主角。笔试里关于HashMap的题,从数据结构、put过程、扩容机制到为什么要用红黑树,几乎每一点都能出题。我把高频考点整理成一张表,方便你直接对照复习:
| 考察点 | 关键结论 | 常见坑 |
|---|---|---|
| 底层结构 | JDK 8后是数组+链表+红黑树 | 误以为JDK 8之前就有红黑树 |
| 默认容量 | 16,负载因子0.75 | 误以为默认容量是10 |
| 扩容机制 | 达到阈值后扩容为原来的2倍 | 漏掉“扩容后元素重新分布”的过程 |
| 链表转红黑树 | 链表长度>=8且数组长度>=64 | 忽略数组长度条件 |
| 线程安全性 | HashMap是线程不安全的 | 以为HashTable能完全替代ConcurrentHashMap |
链表长度为8才转红黑树这个点,笔试和面试都很爱问。原因不是拍脑袋定的,而是基于泊松分布的计算:在负载因子0.75、哈希随机性正常的前提下,链表长度达到8的概率已经极低(约千万分之一)。所以转红黑树是为了极端情况下的兜底,而不是常态。
ConcurrentHashMap的原理也要讲清楚。JDK 7是分段锁,JDK 8改成了CAS + synchronized,锁粒度从Segment细化到单个桶的头节点。笔试如果问“ConcurrentHashMap为什么并发性能好”,标准答法是:读操作大部分不需要加锁,写操作锁的粒度是桶而不是整个表。
4.2 MySQL索引与SQL优化的笔试思路
数据库在Java笔试里属于必考板块,但深度通常不会太深。重点考察:索引失效的几种场景、聚簇索引与非聚簇索引的区别、事务隔离级别、MVCC。
索引失效是选择题里的常客。我总结一个速查口诀:最左前缀、范围之后全失效、计算函数不能使、隐式转换要当心。具体来说就是:
- 联合索引
(a, b, c),查询条件必须包含a才能命中。 - 对索引列使用函数、运算,导致无法走索引。
- 字符串列和数字比较时,会发生隐式类型转换,索引失效。
- 使用
LIKE '%xxx'这种前置通配符,索引失效。
事务隔离级别这块,MySQL默认是可重复读。四个级别(读未提交、读已提交、可重复读、串行化)的脏读、不可重复读、幻读问题,笔试常以“哪个隔离级别下不会发生哪些问题”的形式出现。我的建议是画一张二维表:横轴是三种问题,纵轴是四种隔离级别,逐格标记是否可能出现,一劳永逸。
5. 算法题实操解析:从思路到代码
5.1 排序算法的笔试标准实现
搜索热词里有“冒泡排序java”和“快速排序java实现”,这两个排序算法是校招Java笔试的最爱。原因也很简单:足够经典,写起来不长,但能看出考生对边界处理和代码风格的习惯。
先看冒泡排序的完整实现:
public static 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 temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } if (!swapped) { // 本轮没有发生交换,说明已经有序 break; } } }这里有两个笔试最容易扣分的地方。第一,没有判空就进入循环;第二,没有用一个swapped标志做提前退出。这两个点体现了“代码健壮性”和“优化意识”,在实际批改中很加分。
再看快速排序的实现。快速排序的核心是分治,每次选一个基准值,把数组分成小于基准和大于基准两部分,然后递归处理。基准的选择方式直接影响最坏时间复杂度,笔试里推荐三数取中法,而不是每次都取第一个元素。
public static 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 temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; i++; j--; } } quickSort(arr, left, j); quickSort(arr, i, right); }很多人在笔试时一紧张,把快速排序的边界条件搞错,最常见的是没有判断i <= j就交换,导致排序结果错误。我建议在练习时,把随机数组、升序数组、降序数组、全相同数组四种输入都跑一遍,能过这四种,笔试基本稳了。
5.2 字符串与链表类题目的通用套路
除了排序,字符串和链表也是高频出题方向。字符串题最常见的套路是双指针和动态规划。比如判断一个字符串是否是回文串,用双指针从两头往中间扫,遇到不相等就返回false。链表题最常见的套路是快慢指针,比如找到链表中间节点、判断链表是否有环,都是一个快指针一个慢指针的事。
以“字符串反转”为例,笔试可能不会直接让你“反转”,而是包装成“给定一个英文句子,反转单词顺序”,比如"I am a student"变成"student a am I"。思路是:先把整个字符串反转,再逐单词反转回来;或者用split(" ")拆开后逆序拼接。注意用split时,连续多个空格会让结果出现空字符串,需要做过滤,这也是一个典型坑点。
链表反转则是一个必须背到肌肉记忆的题目,因为笔试编程题常考:
public ListNode reverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode nextTemp = curr.next; curr.next = prev; prev = curr; curr = nextTemp; } return prev; }这段代码的核心是“记住下一个节点,再断开当前节点的next指向”。我批过不少卷子,很多人写到这里就把nextTemp这一步丢了,导致链表断链。记住了——在修改当前节点next之前,必须先保存下一个节点,这是链表操作的铁律。
5.3 手写代码的边界条件与隐形扣分点
很多人笔试程序跑不通不是因为核心逻辑错误,而是边界条件没处理好。我在实际批改校招笔试题时,看到过太多次这些情况:
- 数组为空或长度为0时,直接报
java中数组越界异常。解法是在方法入口加判空。 - 递归算法没有终止条件,或者终止条件写错,导致无限递归栈溢出。
- 整数溢出。比如求两个整数的平均值用
(a + b) / 2,当a和b都很大时可能溢出,正确写法是a + (b - a) / 2。 - 节点为null时直接访问
node.next,导致空指针异常。
从规范角度来说,我建议笔试时养成一个固定习惯:写完核心逻辑后,用三个特殊输入检查一遍——空输入、最小规模输入、极端大输入。这能在不增加太多时间成本的情况下,显著降低低级错误率。
6. 高频报错与避坑指南
6.1 本地能跑、笔试系统报错的经典场景
如果你准备过校招笔试,大概率碰到过这种诡异情况:同一个代码,本地IDE里跑得飞起,一粘到笔试系统就报错。我根据这几年跟同学们的打交道经验,整理出几个最典型的场景:
场景一:类名或包名不匹配。笔试平台要求你写一个Main类作为入口,你却写成了Test类,编译直接失败。这属于最冤枉的扣分。提交前一定要看一眼题目要求的类名。
场景二:编译器版本不一致。本地用的是JDK 17,笔试平台还是JDK 8,代码里用了var关键字或者List.of()这种新API,编译后报错。我的建议是投递Java岗位时,本地环境的JDK版本最好不要高于目标公司常用的版本,至少保证代码能在JDK 8下编译运行。
场景三:输入读取方式不对。笔试系统多数用标准输入输出,有些同学用了Scanner读取,结果超时了,特别是输入规模很大的时候。这种情况推荐用BufferedReader来读取,性能比Scanner更高。你可以在平时的练习中就养成这种习惯。
场景四:lombok相关报错。搜热词里有一条java: you aren't using a compiler supported by lombok, so lombok will not wo,这在新版本JDK中很容易遇到。如果你本地装的是JDK 17以上的版本,而项目里的Lombok版本过低,就会报这个错。解法是升级Lombok版本到1.18.30以上。在笔试环境里如果遇到类似的编译错误,第一时间看是不是IDE或JDK版本的问题。
6.2 环境配置基础问题排查
热词搜索里出现了“java环境变量配置”“java环境变量配置详细教程”“vscode运行java报错乱码”这几条,说明环境问题对很多新手来说很致命。我简单提一嘴:
Windows下配置Java环境变量,核心是设置JAVA_HOME指向JDK安装目录,然后在Path中新增%JAVA_HOME%\bin。配置完后,命令行输入java -version和javac -version都能正常输出版本号,才算配置成功。如果java能用但javac不能用,说明JAVA_HOME没配对,或者Path里多了一个旧版本的Java路径。
关于VSCode运行Java乱码,通常是文件编码不一致导致的。把VSCode右下角编码改成UTF-8,同时在settings.json里加上"java.debug.settings.consoleEncoding": "UTF-8",问题一般能解决。这类问题虽然不考,但会白耗你半小时时间,笔试前一定要把环境搞定,提前把笔试平台的在线IDE试一次。
6.3 背八股文的正确姿势
最后聊聊“java面试八股文”到底要不要背。我的态度是:要背,但更重要的是理解。单纯背答案,笔试客观题也许能蒙对几分,遇到输出题和简答题就会原形毕露——因为你不知道答案背后的推导过程。
我刚开始复习时也走过弯路,把HashMap扩容机制背得滚瓜烂熟,结果面试官问了一个“为什么负载因子是0.75而不是0.5或者1.0”,我当场卡壳。后来我想明白了,这种问题的答案不在背的结论里,而在对数据结构和概率论的推导中。0.75是时间成本和空间成本的折中。负载因子太高(比如1.0),空间利用充分了,但哈希冲突概率大,查询效率下降;负载因子太低(比如0.5),哈希冲突少了,但浪费空间,扩容频繁。
同理,ConcurrentHashMap为什么弃用分段锁改用CAS+synchronized,不仅仅是因为代码简单,更关键的是锁粒度细到单个桶后,锁竞争的概率大幅降低。这类“为什么”的认知,是八股文背不来的,只能靠动手实验和深读源码去体会。
7. 实战复盘:我复习这套题时的具体安排
如果你现在正准备秋招,我给你一套可以直接照抄的复习计划,按时间来排。
第一阶段(1到2周):过基础,建体系。选一本Java核心卷或者一套系统的java学习路线资料,把基础语法、集合、IO、异常、泛型、反射这些过一遍。不是精读,而是快速建立知识地图。这个阶段的目标是,提到任何一个基础概念,你能说出它属于哪个板块、用来解决什么问题。工具方面,建议在本地搭好Java环境,装好IDE,确保能够一键编译运行。
第二阶段(2到3周):刷真题,做错题本。开始刷往年校招笔试题,重点刷集合、并发、JVM、SQL、算法这五个板块。准备一个错题本,不用手写,用Markdown文档整理就好。每一道错题,不只记正确答案,还要记下“我当时为什么选错”和“正确思路是什么”。这是从“低质量勤奋”到“高质量复习”最关键的一步。
第三阶段(1周):手写代码专项。每天固定手写3到5个算法题,排序、二分、链表、二叉树、动态规划、字符串处理各挑几道。手写的意思不是用IDE,而是在纸上或者文本编辑器里写,不依赖自动补全。这一周的目的是把手感练出来,同时训练自己的代码风格和边界条件意识。
第四阶段(考前2到3天):全真模拟。按照笔试的时间限制,完整地做一套模拟题。重点是练节奏:客观题控制在多少分钟内完成,编程题每题预留多少时间,遇到不会的题要不要先跳过去。我见过太多人死在时间分配上:前面选择题抠太久,后面编程题只剩10分钟,结果能拿的分全丢了。
这套安排不一定是最优解,但它符合“先建体系、再集中刷题、最后模拟实战”的复习规律。你可以在执行过程中根据自己的弱项做微调,但主线不建议换。
8. 笔试系统与在线判题的操作细节
很多第一次参加校招笔试的同学,会把大量精力放在复习上,却忽略了笔试平台本身。这其实是个很大的误区。我在实际笔试中吃过亏,也见过同学因为不熟悉在线IDE导致代码写对了却交不上去。
先说输入输出的问题。现在校招笔试大多用ACM风格的标准输入输出,常见格式是:第一行一个整数T表示测试用例数量,接下来T组数据;或者第一行两个整数n和m表示数据规模。这类题的关键是严格按照题目描述的输入格式去解析,多读一个空格、少读一个换行都可能导致结果不对。推荐用BufferedReader读取,输出用StringBuilder统一拼接再一次性System.out.println,能有效减少IO开销。
再说提交按钮和样例测试的关系。你点“运行样例”通过了,不代表所有测试用例都能通过,因为样例只是最简单的验证数据。在线判题系统里通常有隐藏测试用例,会专门覆盖大数、空输入、边界值这些情况。所以在考试时,做完代码后一定要自己构造几个边界输入跑一遍,而不是看到样例通过就提交。
还有一个容易忽略的点:空间和时间复杂度超标。在线判题系统对运行时长有硬性限制,比如Java通常给2秒。如果你用三层循环解决了一个本可以用哈希表解决的题,数据规模一大就超时。所以笔试时拿到算法题,先算一下数据规模:n是10^3还是10^6,直接决定了你能用什么复杂度的算法。n为10^5以上时O(n²)基本必挂,必须换O(nlogn)或O(n)的思路。
9. 从笔试到面试的思维衔接
笔试通过只是第一关,但它和后面的技术面试是有很强关联性的。为什么我强调笔试复习要去理解“为什么”而不仅仅是“是什么”?因为面试官在技术面时,大概率会从你笔试中做错的题、或者做得好的题切入考察。
比如你笔试里HashMap的题答对了,面试官可能会继续追问:“如果我在多线程环境下用它,会出什么问题?ConcurrentHashMap是怎么解决的?JDK 8的ConcurrentHashMap有什么改进?”这一连串问题就是笔试的延伸。如果你在复习时只背了HashMap的结论,没有真正理解它的设计思想,面试官多问两个“为什么”,你就露馅了。
同理,如果你笔试中快速排序的代码写得很好,面试官可能会问你:“快排最坏时间复杂度是多少?什么情况下出现?如何避免?堆排序和快排在实际工程里你会选哪个?”这些都是基于笔试知识的深度扩展。
我在实际面试别人时,经常用这个思路:先问基础概念,再问场景设计,最后问优化方向。一个能把HashMap的负载因子、扩容机制、红黑树转换条件讲透的人,大概率也能把其他集合类融会贯通。因为他的知识是成体系的,而不是碎片化的。
这也是为什么我一直强调,复习不要只背java面试题,要背它的延伸逻辑。每一个知识点,你在复习时都多问自己一句“为什么这样设计”“解决了什么问题”“还有没有更好的方案”,技术面时你会感谢这个习惯。
10. 一些值得长期坚持的Java学习习惯
不管你是正在准备秋招的应届生,还是已经在职的初级开发,有几件事我觉得值得长期坚持,它们能帮你少走很多弯路。
第一,保持手写代码的习惯。不是让你平时写业务代码时不用IDE,而是每周抽一点时间,在纸上或者纯文本编辑器里手写一些经典算法和数据结构。这个习惯能让你在笔试时不依赖IDE的自动补全也能写出完整代码。我从本科开始坚持到现在,在多次笔试和面试的手写代码环节都因此受益。
第二,看源码时先看类注释和核心方法注释,再看成员变量,最后看关键方法的实现。很多人打开源码喜欢从第一个方法往后看,结果越看越晕。以HashMap为例,先看类注释里的“基于哈希表实现的Map接口”“允许null键和null值”“非线程安全”,再看table、size、threshold这些成员变量,最后再看putVal和resize的代码。从上到下,先从构建宏观认知再深入细节,效率翻倍。
第三,把所有学过的知识点都记成了一份“为什么”清单。我复习时有一个习惯,每学一个知识点,就在文档里写三行:是什么、为什么这样设计、典型的应用场景。不要小看这三行,它逼着你从表象走到原理,而原理恰恰是面试官最想听到的东西。
在货拉拉这套题所代表的校招Java笔试中,真正能拉开差距的,不是我上面说的某一个知识点,而是你是否具备系统化的学习方法和主动思考的习惯。技术会过时,框架会更新,但这些底层的操作系统能力,在职场上会持续发挥作用。