“货拉拉2018秋招java工程师笔试题卷二(B)”,我在整理面试资料时翻到了这套老题。说实话,2018年的卷子放到今天看,基础考点依然非常能打,甚至比现在很多花里胡哨的八股文汇总更贴近真实工作场景。货拉拉的业务核心是撮合货运需求与运力资源,后端大量使用Java技术栈,所以这套笔试题基本围绕Java工程师的日常基本功展开:集合、并发、JVM、Spring、MySQL,再加一两道手撕算法。卷B是平行卷,和卷A不同题但同难度,主要用来防作弊和增加题库随机性,所以它的考点分布和出题权重非常有代表性。
这篇文章我打算换个角度来写,不搞“逐题背答案”那种没营养的整理,而是直接拆解这套卷子背后的考察逻辑、每类题型的踩分要点,以及我当年做这类笔试时总结出来的实战套路。不管你现在是在准备校招笔试、跳槽面试,还是单纯想检验一下自己的Java基础扎不扎实,这篇都能帮你把散落的知识点串成一条清晰的复习主线。
1. 这套笔试题的考情盘点与出题思路拆解
1.1 为什么“卷二(B)”值得细看
很多求职者刷题时只盯着最新年份的面试题,觉得2018年的题早过期了。这个想法在Java这种语言上其实不太成立。Java的基础特性十年二十年的变动都很小,集合、并发、JVM这些核心知识,不同年份考来考去就那么几个关键点。
货拉拉这套卷B有特别典型的参考价值:它是一家互联网物流平台在业务高速扩张期,针对校招Java工程师出的笔试题。这个时间段选得很微妙,公司正处于需要大量能快速上手干活的后端工程师的阶段,所以题目不像某些大厂那样偏重算法难题,也不会像小作坊那样只问CRUD。它考察的是一个“来了就能写业务代码,同时还知道为什么这么写”的Java工程师。
1.2 整张卷子的难度分布和采分点布局
从卷B的整体结构来看,题量控制在60到90分钟能完成的范围,题型一般包含选择题、简答题和一两道在线编程题。难度分布大致呈现“30%基础送分、50%进阶拉分、20%压轴选人”的比例。
- 基础送分题:主要考察Java语法细节、集合类的继承结构、异常处理机制,对有认真学过Java基础的人来说属于白给分。
- 进阶拉分题:集中在多线程同步、JVM内存模型和垃圾回收、Spring的IoC/AOP原理、SQL索引优化这些“八股文重灾区”。这类题的目标不是考你会不会背概念,而是看你能不能把概念讲清楚,比如“Synchronized和ReentrantLock的区别”这种题,会背和真懂,一眼就能分辨。
- 压轴算法题:通常是一道不复杂的算法题,常见的有链表反转、快排手写、字符串处理、TopK等。不考特别偏的算法,重点考察基础编码能力和边界条件意识。
我当时做完这套题的直观感受是:选择题虽然不难,但坑很多,几乎每个选项都设置了一个“看似对实则错”的陷阱;简答题的发挥空间很大,同一个知识点,深度不同得分完全不同;算法题反而最轻松,因为考察的算法都是复习过几百遍的常规题。
2. Java基础与集合部分的重点考点拆解
2.1 面向对象三件套:多态、继承、封装在笔试题里的考法
面向对象是Java笔试题永远绕不开的主题。卷B在这个板块的考察方式一般是选择题穿插几个简答题,表面上是考概念,实际上考的是“你是否真的在写代码时用过多态和继承”。
最常见的一个考题陷阱是要求判断下面代码的输出结果:
class Father { public void method() { System.out.println("father"); } } class Son extends Father { @Override public void method() { System.out.println("son"); } public void method(String str) { System.out.println("son: " + str); } } Father obj = new Son(); obj.method(); // 输出什么?答案是“son”。这里考的是Java动态绑定(动态分派)机制:重写方法在运行时根据对象实际类型调用,重载方法则在编译期根据引用类型决定。很多人概念背得滚瓜烂熟,但一看到Father obj = new Son()这种写法就懵了。其实一句话就能讲透:引用类型决定能调哪些方法,实际对象类型决定方法怎么执行。
总结一下这个板块的备考要点:
- 多态的三个必要条件:继承、重写、父类引用指向子类对象。
- 重载和重写的区别,从编译期/运行期、签名、返回类型修饰符等维度对比记忆。
- 构造方法、静态代码块、成员初始化块的执行顺序,这是选择填空常客,原则是“先静态、后成员、再构造,先父类、后子类”。
- 抽象类和接口的设计差异,Java 8之后接口默认方法对两者边界造成的模糊。
2.2 集合框架:HashMap为什么是笔试钉子户
集合类题目在Java笔试题里的地位,就像“鸡蛋灌饼”在早餐界的地位——不一定最惊艳,但每次都会出现。而在所有集合类题目中,HashMap是当之无愧的出题王者。卷B考HashMap几乎是可以确定的,区别只在于考得深还是浅。
初级考法:HashMap和Hashtable的区别。这题不能只答“一个线程安全一个不安全”,要从几个维度完整展开:
- 线程安全性:HashMap非线程安全,Hashtable线程安全(方法级synchronized)。
- 键值是否允许null:HashMap允许一个null键和多个null值,Hashtable不允许任何null。
- 初始容量和扩容逻辑:HashMap默认容量16且扩容后容量为2的幂次方增长,Hashtable默认容量11,扩容为2倍+1。
- 底层数据结构:HashMap在JDK 1.8之后是数组+链表+红黑树,Hashtable是数组+链表。
高级考法:HashMap的put流程,在1.8之后有一个标准流程描述——先对key的hashCode做扰动运算(高16位异或低16位),然后(n - 1) & hash计算索引;如果该位置为null直接插入;如果存在节点则判断key是否相同,相同则覆盖;如果该位置是红黑树节点就走红黑树插入;如果是链表节点则尾插遍历,链表长度超过8且数组容量大于等于64时转红黑树。这段描述基本是Java面试的标准参考答案,但我在笔试时的经验是:不要只背流程,要把“为什么这样设计”也写出来,比如为什么用尾插法(避免1.7头插法在并发扩容时形成环形链表)、为什么链表长度阈值是8(泊松分布下链表长度达到8的概率极低)、为什么扰动运算要用异或(让高位也参与索引计算,降低哈希碰撞概率)。
2.3 异常、泛型与常用类的易错点
这部分的题一般是选择题,难度不大但陷阱密集。先说异常体系,核心要理清受检异常(Checked Exception)和非受检异常(RuntimeException/Error)的区别。考法通常是给你几个异常类让你归类,或者判断某段代码能否编译通过。
需要记住的典型例子:
IOException、SQLException是受检异常,必须捕获或向上抛出。NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException是非受检异常,编译器不强制处理。Error代表JVM层面的致命问题,比如StackOverflowError、OutOfMemoryError(也就是网上经常看到的java: outofmemoryerror: insufficient memory),程序一般无法处理也不应该处理。
泛型这个知识点,笔试经常考察类型擦除。如果你理解泛型是编译期概念,运行时会擦除为原始类型(如List<String>变成List),那基本所有泛型陷阱题都能看破。常见考点:
List<Object>和List<String>没有继承关系,编译期就报错。List<? extends T>只能读不能写,List<? super T>只能写不能读。- 泛型不能用在静态上下文中,因为类型参数是实例级的。
常用类这块,重点看String相关的不可变性、String、StringBuilder、StringBuffer三者的区别:String不可变所以拼接会产生新对象,StringBuilder线程不安全但性能好,StringBuffer在StringBuilder基础上加了synchronized所以线程安全但性能稍差。这个知识点几乎和HashMap一样高频,因为String在实际业务中确实无处不在。
3. 并发与JVM:Java工程师拉开差距的分水岭
3.1 多线程题目的常见考法与答题思路
并发这块是卷B的分水岭,也是很多候选人丢分的重灾区。选择题通常是考察synchronized、volatile、线程池、锁机制的基础认识,简答题则往往要求对比两种并发机制的异同。
先说synchronized和ReentrantLock的区别。这道题在Java面试题里,热度堪比娱乐圈的顶流八卦。答题时我建议分五个层面展开:
- 实现机制:synchronized是JVM层面的关键字,基于Monitor对象实现,自动加锁释放锁;ReentrantLock是JDK层面的类,基于AQS(AbstractQueuedSynchronizer)实现,需要手动加锁解锁。
- 是否可中断:synchronized不可中断,ReentrantLock可以用
lockInterruptibly()实现可中断等待。 - 锁获取是否能超时:synchronized不能,ReentrantLock可以用
tryLock(long timeout, TimeUnit unit)实现超时等待。 - 公平性:synchronized只能是非公平锁,ReentrantLock可以设置为公平锁。
- 条件变量:synchronized用
wait()/notify()配合,ReentrantLock用newCondition()可以创建多个条件队列。
补充一个容易忽略的点:从Java 6开始,synchronized经过锁升级之后(无锁→偏向锁→轻量级锁→重量级锁),性能已经不弱于ReentrantLock了,所以在面试中回答性能差异时要强调“早期有差距,现在差距已经很小,选择更多从功能维度考虑”。
volatile也是常客。核心答两点:可见性和有序性(禁止指令重排),但不保证原子性。快速把它讲清楚的方式是:一个线程改了值,另一个线程能马上看到,这依赖于JMM的“主内存-工作内存”模型;同时volatile通过内存屏障防止指令重排,典型应用是DCL(双检锁)单例中的private static volatile Singleton instance。
线程池这块主要考察执行流程:核心线程满了去队列,队列满了创建非核心线程,达到最大线程数了走拒绝策略。常见的四种拒绝策略要能说得出区别:
AbortPolicy:直接抛异常。CallerRunsPolicy:让调用者所在线程执行任务。DiscardPolicy:直接丢弃任务不抛异常。DiscardOldestPolicy:丢弃队列里最老的任务,重试提交当前任务。
3.2 JVM内存区域与GC策略考题
JVM相关的题考察的是你平时有没有关注过线上应用占用、OOM排查这些问题。卷B在这个方向一般考两类:一是内存区域划分,二是垃圾回收算法和收集器选择。
JVM内存区域要能画出完整的运行时数据区结构:程序计数器、虚拟机栈、本地方法栈、堆、方法区(1.8之后元空间替代了永久代)。笔试常考的知识点是:哪些区域会抛OutOfMemoryError,哪些会抛StackOverflowError,哪些是线程共享的,哪些是线程私有的。
- 堆和方法区:线程共享,可能OOM。
- 虚拟机栈和本地方法栈:线程私有,栈深度不够时抛
StackOverflowError,动态扩容无法申请到足够内存时抛OOM。 - 程序计数器:线程私有,唯一不会OOM的区域。
GC这块要清楚对象判定“死亡”用的是可达性分析算法,而不是引用计数(因为引用计数解决不了循环引用问题)。GC Roots包括:虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。
垃圾回收算法有标记-清除、标记-复制、标记-整理,需要知道各自的优缺点和适用场景。分代收集里面,新生代用复制算法(因为存活率低),老年代用标记-清理或标记-整理(因为存活率高)。
GC收集器按照“从Serial到G1再到ZGC”的发展线梳理就行,重点记忆各收集器的目标:Serial追求单线程简单可靠、Parallel追求吞吐量、CMS追求低停顿、G1追求可控停顿时间且能管理大堆、ZGC追求极低停顿时间。现在很多公司面试还会追问G1的Region划分和可预测停顿模型,这是加分项。
4. Spring、数据库与框架类题目深挖
4.1 Spring IoC/AOP的笔试考察角度
作为Java后端开发的事实标准,Spring是笔试题里的重头戏。卷B简答题几乎必然会涉及Spring两大核心思想:IoC(控制反转)和AOP(面向切面编程)。
IoC这里要避免只答“创建对象的权利交给容器”。更深的说法是:IoC是一种设计思想的落地,通过将对象创建、依赖组装、生命周期管理的控制权从对象本身转移到外部容器,从而降低对象之间的耦合度。以前你需要new AppleService()然后再手动设置它的依赖;现在通过构造器注入或者@Autowired字段注入,由容器帮你组装这些关系。
面试时怎么把IoC讲得不八股?我的建议是结合项目讲:当你在类里注入一个服务时,你关心的是这个服务提供了什么能力,而不关心它怎么初始化、什么时候销毁。这种“面向接口编程”的优雅,正是Spring容器的价值所在。
AOP的考察通常结合Spring事务管理或日志切面来问,核心理解三个概念:
- 切面(Aspect):横切关注点模块化的类,比如
@Aspect注解加的日志切面。 - 通知(Advice):切面在何时执行什么动作,有前置、后置、环绕、异常等五种通知类型。
- 切入点(Pointcut):匹配哪些方法需要执行通知。
问到Spring事务,经常考的是事务失效的场景,这些基本是实际工作中踩过的坑:
- 方法不是public的导致事务失效。
- 同类内部调用,即
this调用绕过代理导致事务失效。 - 异常被catch后没有抛出,导致事务不回滚。
- 抛出的不是RuntimeException,而是受检异常,且没指定
rollbackFor。 @Transactional加在非Spring管理的方法或类上。
4.2 MySQL索引与事务的关键得分点
数据库在笔试中的地位和Spring平起平坐,货拉拉的业务是匹配订单和运力,查询场景多、数据量大,MySQL索引和事务是必然考察方向。
索引的常见考法是给一条SQL,让你分析索引是否生效。这里有一些硬核的点必须掌握:
- B+树索引的结构特点:非叶子节点只存索引键值,叶子节点存全部数据,叶子节点之间通过链表连接,所以范围查询效率极高。
- 联合索引遵守最左前缀原则。字段顺序不同,索引的利用效率完全不同。
- 覆盖索引是指索引本身包含了查询所需的全部字段,可以直接从索引返回,省去回表。
- SQL语句中常见的索引失效场景:对索引列使用函数或计算、隐式类型转换、LIKE以
%开头的模糊查询、使用OR连接非索引列、索引列上有NULL条件等。
事务隔离级别也是一道经典题。MySQL InnoDB默认隔离级别是REPEATABLE READ,但面试经常考的是四种隔离级别分别解决什么问题:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB通过MVCC+间隙锁解决) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
这里有一个大多数人答不好的点:InnoDB的REPEATABLE READ下到底有没有幻读?答案是:在快照读(普通SELECT)下MVCC已经解决了幻读;在当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)下,通过间隙锁/临键锁解决。所以严格说,InnoDB在RR隔离级别下基本不会出现幻读。
MySQL的锁机制也是高频考点。按粒度分有表锁、行锁、间隙锁。在RR隔离级别下,InnoDB通过Record Lock(记录锁)+ Gap Lock(间隙锁)组合成Next-Key Lock来防止幻读。锁这块建议配合实际场景理解,比如“一个UPDATE语句在事务中已经给一堆行加了X锁,另一个事务能否插入新数据”,这类题本质上考的是你对Gap Lock范围的理解。
5. 手撕代码题:常见算法题的解题思路与Java实现
5.1 链表与数组类算法题
卷B的在线编程题一般难度中等偏基础,但要求写出干净可运行的代码。链表反转是最高频的题目,没有之一。我在给别人做面试辅导时,每次都叮嘱:这道题必须做到闭着眼睛都能写出来,因为它是链表类题目的“母题”。
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; }这段代码的关键是记住三步走:先保存下一个节点、把当前节点指向前驱、移动前驱和当前指针。顺便提一句,递归版本也要会写,万一面试官追问“能不能用递归”呢。
数组类题目常见的还有“移除元素”、“合并两个有序数组”、“求数组中两个数的最大和”这类简单题。当时笔试常出现的是合并两个有序数组,这题要求合并到第一个数组中且不开辟额外空间,核心思路是从后往前遍历,因为第一个数组末尾预留了空间,从前往后会覆盖还没处理的元素。
public void merge(int[] nums1, int m, int[] nums2, int n) { int i = m - 1; int j = n - 1; int k = m + n - 1; while (i >= 0 && j >= 0) { nums1[k--] = nums1[i] > nums2[j] ? nums1[i--] : nums2[j--]; } while (j >= 0) { nums1[k--] = nums2[j--]; } }附带一提,冒泡排序、快速排序这类基础排序算法也经常要求手写。快速排序的Java实现建议背过:
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 pivot = arr[right]; int i = left; for (int j = left; j < right; j++) { if (arr[j] < pivot) { swap(arr, i, j); i++; } } swap(arr, i, right); return i; }5.2 字符串与场景模拟题
字符串是算法题的另一个出题大户。常见的有判断回文串、字符串中的第一个唯一字符、实现strStr()等。
这里说一下字符串题目的通用技巧:先判断边界条件(null、空串、长度超限),再考虑能否用双指针、滑动窗口、哈希表优化。尤其是滑动窗口,在处理“最长无重复子串”这类题时,比暴力破解效率高出一个数量级,而且代码模板很固定。
public int lengthOfLongestSubstring(String s) { Map<Character, Integer> map = new HashMap<>(); int left = 0; int maxLen = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (map.containsKey(c) && map.get(c) >= left) { left = map.get(c) + 1; } map.put(c, right); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; }算法题想拿满分,我总结了一个“四步法”:第一步,明确输入输出的边界;第二步,想清楚暴力解法再优化;第三步,写代码时注意数组越界(这类笔试题的运行时错误提示里,数组越界异常出现频率极高);第四步,写完立刻用边界用例自查,比如空数组、单个元素、最大值。
还有个实用建议:笔试题的在线判题环境和你本地IDE不一样,往往不会有智能提示,更不会有代码补全。所以平时训练时就别太依赖IDE的自动补全,尽量多用记事本或者白板写代码,这样才能在笔试时游刃有余。
6. 在线笔试实操技巧与排查心得
6.1 笔试环境与编译调试的坑
在线笔试和现场面试写代码完全不同。现场面试你可以边写边向面试官解释思路,在线笔试只有你一个人对着屏幕,编译器报错又泛泛,很容易卡壳。这里分享几个我实际踩过的坑。
第一个坑是语言环境版本不一致。本地用的JDK 11,在线编辑器可能还在用JDK 8。如果你用了var关键字或者Java 11的新API,在JDK 8环境直接编译不过。所以笔试时尽量用保守的写法,避免用最新版本特性。另外一个常见报错是“警告: 源发行版 17 需要目标发行版 17”这类版本不匹配问题,本质是编译器默认级别和你环境配置不一致,写算法题时尽量不用涉及到运行环境配置的写法。
第二个坑是输入输出的格式。在线判题系统通常要求从标准输入读取数据,很多手生的人卡在Scanner的next()和nextLine()混用上。一个经典问题是:先用nextInt()读数字,再用nextLine()读字符串,结果读出来是空串。原因在于nextInt()只读取数字,不会消费掉末尾的换行符,这个换行符就被nextLine()吃掉了。解决办法是在两者之间补一个scanner.nextLine()。
第三个坑是时间复杂度的预估。2018年的在线判题系统普遍对时间限制比较严格,用O(n²)的暴力解法很容易超时。通常数据规模在10^5以上就要考虑O(nlogn)甚至O(n)的解法。笔试时间安排上,我建议先扫一遍全部题目,按“会做的先做、分值高的先做”原则分配时间,不要在一道选择题上纠结超过5分钟。
6.2 简答题的踩分套路
简答题是最容易被忽视的拿分项。很多人觉得简答题只要写几个关键词就给分,结果写得干巴巴的,得分自然不高。根据我的经验,简答题的踩分要点是“关键词先行,展开在后”。
比如说问“HashMap的底层实现”,第一行先写“数组+链表+红黑树”,这是核心关键词。然后展开说明什么时候转链表、什么时候转红黑树,再补充扩容机制和索引计算。这样既保证踩分点齐全,又显得有深度。
遇到不会的题,有一个“兜底策略”:写出你能想到的所有相关知识点,尽量往问题上靠。比如问“并发编程的挑战”,就算你不记得完整框架,至少能写出“可见性、原子性、有序性”这三个关键词,然后展开说明各自的含义。在踩分逻辑下,写一点就有一点分,空白一定没分。
还有一个小技巧:简答题写完后,如果时间充裕,可以回头补充例子。比如描述完volatile可见性后,加一句“典型应用是DCL单例中防止指令重排导致半初始化对象被读取”。这种例子能让你的答案和其他只会背概念的候选人瞬间拉开差距。
6.3 考后的复盘方法
笔试结束不代表任务完成,复盘才是提升的关键。我一般的做法是:考完当天趁记忆新鲜,把还记得的题目和你的回答记录到文档里,然后逐个查漏补缺。
复盘时重点做三件事:
- 整理错题:哪些知识点模棱两可,明确标注并重新复习。
- 整理表述:哪些题目虽然答对了但表述不清晰,重新组织成更精确的语言。
- 整理遗漏:哪些题目完全不会,针对性地去填补知识盲区。
如果你发现自己在并发或JVM的题目上连续丢分,那就要警惕了,这说明你的知识体系在工作后接触的高级场景还太少,建议系统性地找一套网课或经典书籍重新过一遍。如果集合和基础部分丢分,那大概率是基础语法不够扎实,建议回头刷一遍官方文档和基础书籍。
根据我个人的经验,一套卷子做完之后花两三天去复盘整理,效果远比盲目做十套新题更好。因为笔试考察的知识点高度收敛,翻来覆去就是那些核心内容,吃透一套,比囫囵吞枣十套管用得多。
再说一个很多人都忽视的点:在线笔试的编程环境通常不小心中断就交卷了,写代码过程中要养成时不时“手动保存”的习惯。另外,如果允许自行选择语言,别贪心选一个自己最熟的语言,虽然Java岗位要求用Java,但有的平台允许选多种语言,使用最熟练的语言通过率一定高于半生不熟的Java,没必要为了所谓的“岗位要求”拿自己的短板冒险。
7. 这套题对现代Java工程师的迁移价值
可能有人会说,2018年的题,现在都过去好几年了,技术栈变了很多,还有必要看吗?我觉得非常有必要,因为Java生态的基础底座一直没变:JVM还在,并发模型还在,Spring还在,MySQL还在。真正变了的是工具链的丰富度和业务场景的复杂度,而笔试考察的核心从来都是基础而不是新潮。
当年这套卷B考察的知识点,在你现在的工作中依然天天遇到:
- 写业务代码时你要理解HashMap的线程不安全性,所以在多线程环境下得用ConcurrentHashMap。
- 排查线上OOM时,你需要懂JVM内存区域划分和GC日志分析,否则面对“
java: outofmemoryerror: insufficient memory”这类报错只能干瞪眼。 - 设计接口时你会用到Spring的AOP做日志审计或权限拦截,事务失效的处理也是老生常谈。
所以,如果你正在准备Java岗位的面试,建议找一套这种早年间的完整笔试题来做一次自我诊断:限时90分钟,不看任何资料,像真正的笔试一样完成。做完之后,你的薄弱环节会暴露得非常清楚,远比漫无目的地刷一百道零散题目更有效率。
关于Java面试,还有一个近两年的明显变化值得提一下:面试官越来越不喜欢只会背八股文的候选人,而是喜欢问“你在项目中实际怎么用的”。这就意味着你复习时不能只背概念,还要主动联想每个知识点在你的实际项目中是怎么体现的。比如,你说你用到了Redis,那就要能答出Redis的过期策略和缓存一致性;你说你负责过订单模块,那就要能说清楚事务隔离级别和锁的配合使用。把知识点投射到项目经验里,面试的通过率会大幅提升。