我手上还留着那年秋招的截图。货拉拉2018秋招Java工程师笔试题卷三(A),当时做完的最大感受是:这套题不偏、不怪,但它特别会"挖坑"——每个考点都看着眼熟,可真下笔的时候,稍不注意就会掉进细节里。今天我把这套题涉及的考点、我当时怎么答的、以及现在回头看哪些地方最值得应届生注意,完整复盘一遍。
这套试卷适合两类人看:一是正在准备Java后端校招、想提前摸底笔试难度的人,二是已经工作但想回头补一补基础、检验自己基本功的人。不管你现在处在哪个阶段,我尽量把每道题背后的考察意图讲透,让你拿到任何一套笔试题,都能快速判断它到底在考你什么。
1. 笔试题整体架构与考点分析
1.1 这套卷三(A)考了什么、为什么这么考
货拉拉2018年那会儿正处于业务快速扩张期,订单调度、司机端、用户端这些系统都在大量招人。笔试出题人很明显不是想难倒你,而是想筛出"基础扎实、能直接上手干活"的人。整张卷子满分100分,题量大概在30道左右,题型包括单选、多选、简答和两道编程题。从知识模块来看,覆盖了Java基础语法、集合框架、JVM、并发编程、Spring、MySQL这些校招笔试题的"标配"范围。
我印象最深的是,这套卷子里没有一道题是纯粹背概念就能答对的。比如它考Java基础时,不是问你"什么是面向对象",而是给你一段代码,让你判断某个方法重载之后到底会调用哪一个。这就要求你在理解重载规则的同时,还得对参数类型转换、自动装箱拆箱这些细节特别敏感。出题人真正想测试的,是你有没有在实际项目中踩过这些坑,而不只是背过书。
从难度梯度上看,选择题部分大概占了40%,难度中等偏易,属于"认真审题就不会错"的级别;简答题和编程题占60%,尤其是两道编程题,一道考手写算法,一道考场景设计,基本就决定了你能不能进入下一轮面试。这套题的价值在于,它把"会背"和"会用"区分得很清楚——你如果只是把八股文背得滚瓜烂熟,但不理解底层原理,编程题很容易露馅。
1.2 考题分值分布与答题策略
我先按照自己的回忆,把这套卷子的知识点分值分布整理成一张表,这样你对重点一目了然:
| 知识模块 | 题型 | 预估分值 | 实际难度 |
|---|---|---|---|
| Java基础语法(String、异常、泛型) | 单选+多选 | 15分 | 中等 |
| 集合框架(HashMap、ArrayList等) | 单选+多选 | 10分 | 中等 |
| JVM内存与垃圾回收 | 单选+简答 | 15分 | 中等偏难 |
| 并发编程(锁、线程池) | 单选+简答 | 15分 | 较难 |
| Spring核心机制 | 简答 | 10分 | 中等 |
| MySQL索引与事务 | 简答 | 10分 | 中等偏难 |
| 编程题(算法+场景设计) | 手写代码 | 25分 | 难 |
这个分值分布是我综合同类互联网公司校招笔试题型推断的,不一定和原始试卷逐题对应,但大方向是合理的。从策略上来讲,90分钟的考试时间,我建议你这么分配:选择题和简答题控制在50分钟内完成,剩下40分钟全部留给编程题。很多同学容易犯的错是在简答题上长篇大论,结果编程题没时间写完,这非常可惜——编程题一题就顶三四个简答题的分值。
还有个小技巧:凡是遇到"以下说法正确的是"这类多项选择题,如果你不确定其中一个选项,宁可少选也不要多选,因为多选错选往往整个题都算错。这套卷子里有好几道多选陷阱题,选项之间只差一个"volatile不保证原子性"这样的关键认知,少选还有分,选错直接归零。
2. 选择题里的高频陷阱:Java基础与集合框架
2.1 面向对象与字符串:那些年容易踩的坑
Java基础选择题是这套卷子的第一个重头戏。我记得有一道题,给了一段用String拼接字符串的代码,问最终创建了几个对象,A、B、C、D四个选项分别是2、3、4、5。这道题其实在大学期末考试里也算经典题,但放到笔试里,它考察的是你对字符串常量池和不可变性的理解深度。String str = "a" + "b" + "c"这种写法在编译期就会被优化成"abc",只创建一个常量池对象;但如果中间夹了变量,比如String b = "b"; String str = "a" + b + "c",编译期无法确定b的值,就会走StringBuilder的append操作,对象数量就变了。
我当时做这套题时的答案是:第一问编译期常量折叠,创建1个对象;第二问走了new StringBuilder(),最终创建了3个对象。出题人的心思其实很明显,就是想看你了不了解编译期优化和StringBuilder的机制。如果你只是背过"String是不可变的"这句话,面对具体代码时还是会蒙。
除了String,面向对象相关题目里还考了一道重载与重写的辨析题。它故意构造了一个父类和子类都有同名方法的结构,然后问你在某个调用场景下走的是哪个方法。答这种题有个稳的思路:先判断方法签名是否完全一致,一致就看有没有@Override注解,有就是重写,受父类引用类型限制;不一致就是重载,由编译时类型决定。笔试时一定不要急躁,先把方法签名写在草稿纸上再判断,正确率会高很多。
另外,异常处理这块它也考了一个很容易忽略的点:try-catch-finally中,如果finally里有return语句,它会覆盖try里的return值。这道题当时答对的人就不多,因为它考察的是字节码层面的执行逻辑,而不是单纯的语法记忆。我建议你把"finally真的会在return之前执行吗"这个问题从字节码角度想透,而不是只记结论。
2.2 集合框架:HashMap扩容与ConcurrentHashMap
集合框架的选择题里,HashMap的相关知识点出镜率极高。这套卷子考了HashMap的默认初始容量和扩容因子,也考了JDK1.7和JDK1.8在插入逻辑上的差异。默认容量16、负载因子0.75,这是一个必背的基础数据,但在笔试里几乎没有直接问数字的,而是给你一段代码,让你推算当元素加到多少个时会发生扩容。我的经验是,这类题务必记住一个公式:扩容阈值 = 当前容量 × 负载因子,到了阈值就会扩容到原来的两倍。16 × 0.75 = 12,所以第13个元素put进来时,HashMap就会触发扩容,而不是等到容量满了才扩。
有一道多选题,选项里混着"HashMap允许null键null值"和"Hashtable不允许null键"这两句话,很多同学看哪个都觉得对,结果多选时把两个都选了。实际上,HashMap的null键能存在是因为它的hash方法对null做了特殊处理,直接把null映射到0号桶;而Hashtable的hashCode方法直接调用了key.hashCode(),null传进去立刻空指针。这个细节看起来小,但在生产中你如果把null键传给了不支持的集合类,线上就是事故。
关于ConcurrentHashMap,这道卷子主要考了它为什么线程安全。老版本JDK1.7的ConcurrentHashMap用的是分段锁(Segment),把整个Map分成16段,每段单独加锁,这样多个线程操作不同段时可以并行。JDK1.8之后换成了CAS加synchronized锁桶头节点的方式,锁粒度更细了。我在写简答题时,特意提到了"读操作通常不加锁,依赖volatile保证可见性"这一点,这也是面试官最想听到的关键。
3. JVM与并发:笔试拉开差距的分水岭
3.1 JVM内存区域与OOM:不只是背概念
这套卷子的简答题部分,有一道几乎年年出现在Java笔试题里的老熟人:"简述JVM运行时数据区,并说明哪些区域会抛出OutOfMemoryError。" 这类题之所以经典,是因为它能同时考察记忆力和理解力。我答题时先画了一个内存分区表格,包含程序计数器、虚拟机栈、本地方法栈、堆、方法区,再逐个标注是否会产生OOM,以及在什么情况下产生。程序计数器不会OOM;虚拟机栈和本地方法栈会抛StackOverflowError,深度不够时才可能OOM;堆和方法区是OOM的高发地,尤其是堆,只要分配的对象超过堆最大容量就会炸。
当年有一个版本的JDK里,方法区还叫"永久代"(PermGen),很多人会把方法区和堆搞混。实际上方法区主要存类元信息、常量、静态变量等,它不在堆里。JDK8以后方法区被元空间(Metaspace)取代,默认不再受堆内存限制,而是受本地内存影响。笔试如果考到这一点,你光说"元空间在本地内存"还不够,最好补一句"类加载器如果一直不卸载,元空间同样可能OOM"。我面试过不少候选人,"元空间"这个名词能说对,但一问到"什么情况下元空间会OOM",就答不上来了,说到底还是没理解类加载器与内存回收的关系。
这道卷子还结合了一把实际生产经验:线上服务频繁报警"java.lang.OutOfMemoryError: Java heap space",问可能是什么原因、怎么排查。我的答案分了三步走:第一步,先看监控确认堆内存使用是否持续上升;第二步,用jmap导出堆dump文件,再用MAT或jhat分析,找到是哪个对象占用了大头;第三步,定位代码,看是否存在大对象、内存泄漏或无限缓存。笔试中能把排查思路写清楚,比堆砌命令更有价值,因为面试官想看到的是你有没有真实的线上问题处理经验。
提示:JVM相关的题,答题时不要只罗列概念名词。加一句"什么情况下会触发、如何排查"会让你的答案立刻提升一个档次。
3.2 线程池参数和锁升级:这道简答题必须拿满分
并发编程部分,这套卷子出了一道很实际的题:让你设计一个线程池,并说明核心线程数、最大线程数、阻塞队列容量怎么设置。我看到这道题时还挺感慨,因为2018年很多校招生都在背"线程池有哪几种",但货拉拉直接考"你怎么设计",说明他们要的是能真正维护高并发系统的工程师。
线程池的核心参数就7个:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(非核心线程空闲存活时间)、workQueue(阻塞队列)、threadFactory(线程工厂)、handler(拒绝策略)。关键不是把这7个参数背出来,而是要知道它们之间的协作关系:提交一个任务时,先判断当前线程数是否小于核心线程数,小于就创建核心线程执行;否则尝试放入阻塞队列;队列满了再看是否小于最大线程数,小于则创建临时线程;如果连最大线程数也达到了,就执行拒绝策略。我当时画了一个简单的判断流程图来回答这道题,面试官反馈说思路很清晰。
关于具体参数设置,我提供了一个经验值:如果是CPU密集型任务,核心线程数设置为CPU核数+1比较合理;如果是IO密集型任务,核心线程数可以设置到CPU核数×2甚至更多,因为IO等待时间占比高,线程可以把等待时间让给其他任务。这套卷子里没有给出具体的机器配置,所以我在答案里写了"需要根据任务类型和机器配置来动态调整,没有一套参数打天下的方案",这样反而显得有工程经验。
还有一道选择题问的是synchronized的锁升级过程。从无锁到偏向锁、轻量级锁、重量级锁,这个知识点在2018年已经是高频考点,放在今天依然不过时。我当时选了偏向锁→轻量级锁→重量级锁这条链路,还特意在选项旁边标注了触发条件:偏向锁会撤销并升级到轻量级锁,当竞争激烈时才会升级成重量级锁。这种题最容易错的是把方向搞反,或者把"锁消除""锁粗化"这种编译期优化混进来。锁消除是JIT编译器检测到某个锁对象不可能被其他线程访问时,直接去掉锁;锁粗化是把多个连续加锁解锁的代码块合并成一次加锁。它们和锁升级完全是两码事。
4. 编程题实战:从手写代码看工程能力
4.1 排序算法与链表操作:这轮题不能丢分
笔试的编程题第一道往往是基础算法题,这套卷子考的是"手写快速排序",并要求说明时间复杂度和稳定性。我到现在都还记得当时在答题纸上写下的那段代码,因为这道题我私下练过不下二十遍。快速排序的核心思想是分治:选一个基准值(pivot),把小于它的放左边、大于它的放右边,然后递归处理左右两个子区间。平均时间复杂度O(n log n),最坏情况O(n²),空间复杂度O(log n),而且是不稳定排序——因为相等元素的相对顺序可能在交换时被改变。
我提供一版我当时写的参考实现,基于经典挖坑法,比较适合笔试场景:
public void quickSort(int[] arr, int left, int right) { if (left >= right) { return; } int base = arr[left]; int i = left; int j = right; while (i < j) { while (i < j && arr[j] >= base) { j--; } if (i < j) { arr[i++] = arr[j]; } while (i < j && arr[i] <= base) { i++; } if (i < j) { arr[j--] = arr[i]; } } arr[i] = base; quickSort(arr, left, i - 1); quickSort(arr, i + 1, right); }除了快排,这套卷子的编程题还考了一道"反转单链表"。反转链表看起来简单,但最容易出错的地方是:你需要在改变当前节点的next指向前,先保存它的后继节点,否则链表就断了。用迭代法的话,维护prev、curr、next三个指针,每次循环把curr.next指向prev,然后三个指针整体后移;用递归法的话,则要明确递归函数返回的是反转后的新头节点,而不是原链表头。笔试时我建议你优先写迭代法,因为递归法虽然代码短,但运行时的调用栈深度容易让人绕晕,而且如果链表很长,递归还会带来栈溢出风险。如果在答题纸上写递归,务必在注释里说明递归终止条件。
4.2 场景设计题:电商订单超时取消与库存扣减
这套卷子的最后一道编程题是一道典型的场景设计题,核心业务是:用户下单后,如果30分钟内未支付,订单需要自动取消并释放库存。这种题在互联网公司的笔试里太常见了,因为它没有标准答案,考察的是你如何把技术方案落到实际业务里。
我当时在答题纸上写的第一版方案用的是定时任务扫描:每隔1分钟扫一次订单表,把超时未支付的订单查出来,批量置为取消状态并回滚库存。这个方案能跑,但明显有性能瓶颈——订单表一旦大了,全表扫描就是灾难。我意识到出题人想听到的是延迟消息或者Redis过期监听之类的方案,所以在答案里又补了一层设计:下单时往Redis写入一个键,过期时间设为30分钟并设置过期监听,由监听方触发取消操作。考虑到Redis过期事件并不能保证100%及时可靠,我又补充了定时任务兜底扫描,形成双层保障。
我总结一下这道题答得好的关键,其实在于"分析问题"而不是"背方案":
- 主动取消:用户主动取消订单,直接调用取消接口,这是最简单的情况。
- 超时自动取消:需要延迟触发机制,可以选定时扫表、RabbitMQ延迟队列、Redis过期事件中的一种或多种组合。
- 库存释放:取消订单前要确认订单状态,防止重复取消导致库存多加。
- 幂等性:用订单号作为唯一标识,取消操作要保证重复调用时不会产生副作用。
库存扣减这道题还有个经典变体:秒杀场景下如何防止超卖。答案一般要围绕"数据库乐观锁/CAS扣减库存"来展开,比如update stock set stock = stock - 1 where id = ? and stock > 0,通过受影响行数判断是否扣减成功。这套卷子在场景题里隐含了这部分要求,因为扣减库存和释放库存是同一套逻辑的正反面,你能把释放库存讲透,扣减库存的思路也就顺带体现了。
5. 数据库与Spring考点:简答题里的保分项
5.1 MySQL索引失效场景与事务隔离级别
数据库这部分的简答题,货拉拉考了"什么情况下索引会失效",这几乎是所有Java面试的保留题目。我按实际经验把索引失效的常见场景整理成了一个清单,笔试时照这个方向答基本不会丢分:
- 对索引列使用函数运算,比如WHERE YEAR(create_time) = 2023,索引会失效。
- 隐式类型转换,比如索引列varchar类型,查询条件写成数字,MySQL会自动转类型,索引失效。
- 最左前缀原则不满足,联合索引(a,b,c)中,如果查询条件跳到b,a没带,那b上的索引就发挥不了作用。
- LIKE以通配符开头,比如'%abc',索引失效;'abc%'前缀匹配仍然可以走索引。
- 使用OR连接多个条件,如果其中一个字段没有索引,整个查询可能放弃索引。
- 索引列上做了空值判断或not in、!=,在部分情况下索引效率也会明显下降。
我当时答这道题时,额外写了一句"索引失效的本质是优化器认为全表扫描比走索引更快,或者走索引无法精确定位数据",这能证明你不是在背清单,而是真的理解B+树索引的原理。B+树的叶子节点是按顺序排列的,一旦查询条件无法利用排序特性,自然就得退化为全表扫描。
关于事务隔离级别,这套卷子也考了一道选择题,默认的MySQL隔离级别是什么。答案当然是可重复读(REPEATABLE READ),InnoDB存储引擎在这个隔离级别下通过MVCC解决了快照读的幻读问题,但当前读(比如SELECT ... FOR UPDATE)仍然可能出现幻读。这个知识点需要区分清楚,因为很多人在面试时张口就说"可重复读解决了幻读",实际上MySQL是在可重复读级别下结合间隙锁(Gap Lock)才做到大部分场景的幻读防护。如果你能把这个边界说清楚,面试官会对你刮目相看。
5.2 Spring Bean生命周期与事务传播行为
Spring相关的简答题,这套卷子考了Bean的生命周期和事务传播行为。Bean的生命周期是一个比较"死"的知识点,但写得好不好,能体现你有没有真正用过。我的记忆方法是把生命周期拆成四段:实例化前、实例化后、初始化前、初始化后。具体来说,Spring在Bean实例化之后会先做属性填充,也就是依赖注入;然后调用各种Aware接口回调,比如BeanNameAware、BeanFactoryAware;接着是BeanPostProcessor的postProcessBeforeInitialization;再走@PostConstruct或InitializingBean的afterPropertiesSet;然后是自定义init-method;最后是BeanPostProcessor的postProcessAfterInitialization,这时候Bean才算真正可以用了。销毁时的顺序则大致相反。
在答题时,我把这套流程写成一段按顺序编号的文字,并且专门标注了"BeanPostProcessor会作用于容器中所有Bean,而不是某一个Bean",这是一个容易被忽略的细节。很多人在实际开发中根本不会手动实现BeanPostProcessor,但Spring的AOP就是通过它实现的,理解了这一点才能真正理解Spring容器是如何工作的。
事务传播行为是Spring面试里的常客,货拉拉这道题考的是一个具体场景:一个方法调用了另一个带有@Transactional注解的方法,问事务会不会合并。答案取决于传播行为,默认的REQUIRED传播级别会合并成一个事务,内层方法如果抛出异常,外层事务也会回滚。但这里有个大坑:如果在同一个类内部调用,即this.methodB(),事务注解是失效的,因为Spring事务是通过AOP代理实现的,内部调用不会经过代理对象,事务就不会生效。这个知识点在笔试的选择题里经常出现,在面试里更是必问,你最好在答题时主动提到"内部调用事务失效"这一点。
6. 笔试复盘:常见问题与备考建议
6.1 当年我踩过的坑和考场急救经验
我在做这套卷子时,有一个比较大的失误:选择题花的时间太多,导致最后编程题写得比较仓促。当时有一道关于HashMap链表转红黑树的判断题,我盯着"链表长度为8时转红黑树"这句话考虑了很久,因为严格来说,不是链表长度一到8就立刻转,而是链表长度达到8且数组容量大于等于64时才会树化。这种细节在笔试里经常出现,但如果你知道这个知识点却拿不准,说明它没真正变成你的"条件反射"。
在考场上,我的急救原则是:遇到卡壳超过2分钟的选择题,先标记跳过,等所有会做的题写完后再回头纠结。因为笔试题量大,时间有限,一道选择题丢了可能只扣2分,但编程题没写完可能直接丢掉十几分。这个策略是我踩过很多次坑之后总结出来的,真心建议你试试。
还有一点关于读题:多选选择题的题干一定要一个字一个字地看。这套卷子的第一道多选题问的是"以下哪些情况会导致ClassCastException",选项里有个"Integer转String",这其实编译都过不了,更不可能抛ClassCastException,反而是个迷惑项。如果只看选项内容去判断对错,很容易被带偏;一定要结合题目要求,看清它问的是"可能"还是"一定",是"运行时"还是"编译时"。
6.2 从一套题看Java校招笔试的复习方法
做完货拉拉这套卷子,我最大的体会是:笔试不是靠死记硬背能通过的,它考察的是你能否把学过的知识串成体系。Java基础、集合、JVM、并发、Spring、MySQL这六大模块不是孤立的,而是相互关联的。比如集合框架里ConcurrentHashMap为什么线程安全,底层是CAS加synchronized,这正好对应并发编程里的锁知识;synchronized的锁升级过程又依赖JVM的对象头布局;对象头布局又属于JVM内存布局的一部分。如果你只背零散的知识点,面对这种环环相扣的题目时很容易露出破绽。
我建议准备校招笔试时,每复习完一个模块,就自己画一张知识图谱,把相关概念之间的引用关系写出来。比如复习并发时,从"原子性、可见性、有序性"这三个特性出发,把volatile、synchronized、Lock、CAS、ThreadLocal这些技术点挂上去,再标注每个技术对应解决了哪个特性。这样做的好处是:考试时不管你遇到哪个角度出的题,都能顺着图谱快速定位考点。
关于刷题,我自己那段时间的做法是,每天固定花一个多小时在牛客网上刷Java方向的笔试题,刷完不是看一遍答案就过,而是把做错的题分类整理到一个错题本里,每周复盘一次。错题本不需要多么花哨,就用手机备忘录记录,但一定要写得具体,比如"HashMap在JDK1.8中先插入后扩容"这个易错点,我会把源码关键路径抄一遍,而不是只记正确答案。通过这种方式,同样类型的坑我不会踩第二次。
6.3 这套题对现在准备Java面试还有没有参考价值
说实话,距离货拉拉2018秋招已经过去几年,但Java后端校招笔试的底层逻辑几乎没有变过。现在的面试仍然在考集合、JVM、并发、Spring这些核心模块,只是题目的包装形式越来越贴近实际场景。比如现在的编程题可能会改成"设计一个限流组件""写一个防重复提交的注解",但考察的基础能力还是那几板斧:数据结构的选用、并发控制、工程化思维。你如果能把2018年的这套笔试题吃透,再去做现在的笔试,会发现大多数题都是"换汤不换药"。
不过有一个明显的变化趋势是,现在笔试对"系统设计"和"线上故障排查"的考察比例变高了。以前考的是"JVM内存怎么分区",现在更多会问"线上频繁Full GC怎么排查"。所以如果你只是刷旧题,不补充实操经验,很容易在进阶题型上吃亏。我建议在复习旧题时,顺手把每个知识点代入到真实生产场景中想一想:这个知识点出问题了,线上会有什么现象?我该怎么定位?把这些想清楚了,笔试面试都能通吃。
最后再分享一个我自己比较受用的方法:遇到好的笔试题,不要只看答案,最好自己用IDE把代码跑一遍,再改几个参数看看结果有什么变化。比如HashMap的扩容题,你可以在代码里设置初始容量为4,循环put 20个元素,打印每次扩容后的容量变化,这种动手验证会让你对知识点的记忆牢固得多。这套卷子的价值也在这里——它是一面镜子,帮你发现自己在Java知识体系上到底还有哪些盲区和薄弱点。