2020届秋招那会儿,我在牛客网上刷到浩鲸科技的Java开发岗笔试邀请。说实话,当时对这家公司的了解不算深,只知道是通信行业出身的软件服务商,业务面很广。抱着多积累经验的心态,我点开了笔试链接。
整套题做下来,我的第一感受是:题目不算偏,但覆盖面非常宽,细节陷阱特别多。它不像一些大厂那样上来就是一道hard级算法题压阵,而是把Java基础、集合源码、并发、JVM、数据库、框架使用、场景设计全部铺开考了一遍。换句话说,它考察的不是你会不会某个冷门知识点,而是你日常学习Java时有没有真正理解那些“最常用”的东西。
这篇文章我就结合当时的笔试经历,把考察到的核心知识点、我当时的答题思路,以及事后复盘发现的丢分点,逐块整理出来。不管你是准备投递浩鲸科技,还是在准备其他公司的Java笔试,希望这篇复盘能帮你少走一些弯路。
1. 浩鲸科技2020届Java笔试整体回顾:题量、结构与应对策略
1.1 笔试形式与题型分布
浩鲸科技这场笔试是在牛客网进行的,总时长印象中是90分钟还是120分钟,题量不算小。题型大致分为四类:
- 单选题:约15题,覆盖Java基础语法、面向对象、异常、常用类库等,每题考察的点都比较细。
- 多选题:约10题,主要考察集合框架、并发包、JVM相关概念,多选少选都不得分,这一点比较折磨人。
- 简答题:4题左右,需要手打文字作答,一般考察的是某个机制的原理,比如HashMap的扩容过程、线程池的执行流程等。
- 编程题:2到3题,题目难度适中,以字符串处理、数组操作、排序为主,不涉及特别复杂的算法。
这个结构透露出的信号很明确:浩鲸科技的笔试更看重基础知识的广度和代码功底,而不是算法竞赛式的思维。如果你把大量时间花在刷leetcode hard题上,而忽略了Java基础源码的深入理解,这场笔试的得分反而不一定高。
1.2 判分逻辑与答题节奏
多选少选不得分这一点,直接影响我的答题策略。对于没把握的多选题,我尽量只选两个最有把握的选项,放弃挑战全对的可能。这样做虽然上限分低一些,但能保住下限。事后和一起笔试的同学交流,确实有同学因为多选贪多,反而把本来能拿到的分丢掉了。
时间分配上,我大致是按这个节奏来的:
- 单选+多选:30分钟
- 简答题:25分钟
- 编程题:30分钟
- 剩余时间检查+补漏:10到15分钟
实际做下来,编程题比我预想的要基础,反而是简答题因为要写清楚“原理说理”,比想象中费时间。建议后续考生给简答题预留更多的时间,不要急着赶编程题。
这里补充一个实操心得:笔试开始前先快速扫一遍所有题目的类型和数量,心里大致有个时间预算。我看到有些同学在一道多选题上纠结了五六分钟,导致后面编程题时间不够,这是最不划算的。
2. 基础语法与JVM考点:送分题与送命题的分水岭
2.1 String、==与equals的经典陷阱
单选里出现了一道很经典的String题目,大概意思是判断几个字符串比较表达式的输出。
这类题的核心在于搞清楚两件事:
==比较的是引用地址,equals比较的是内容(前提是没有重写过)。- 字符串常量池会让直接赋值的字符串在池中复用,而
new String()会创建新的堆对象。
我当时遇到的一个变体是这样的:
String s1 = "java"; String s2 = "java"; String s3 = new String("java"); String s4 = s3.intern(); System.out.println(s1 == s2); // true System.out.println(s1 == s3); // false System.out.println(s1 == s4); // true System.out.println(s3 == s4); // false这里最容易被忽略的是s4 = s3.intern()。intern()方法会尝试把字符串放入常量池,如果常量池中已经有了相同内容的字符串,就直接返回常量池中的引用。因为s1已经让常量池中存在了"java",所以s4拿到的就是s1那同一个引用。
这种题考察的是对JVM字符串常量池机制的掌握程度,不光是背结论,还要知道背后的存储逻辑。笔试前我刚好系统梳理过这个知识点,所以做起来比较顺畅。
2.2 重载与重写、静态绑定与动态绑定
多选里有一个题考察的是重载和重写的区别,选项涉及的维度包括:方法名是否相同、参数列表是否相同、返回值是否能不同、权限修饰符的要求、异常声明的要求等。
区分重载和重写的关键在于理解它们的设计初衷:
- 重载是同一个类中多个方法使用相同方法名、不同参数列表,目的是让同一个操作在不同输入类型下表现不同,属于编译期多态。
- 重写是子类重新实现父类的方法,要求方法签名完全相同(参数列表和返回类型兼容),属于运行期多态。
有个容易忽视的知识点:重写时的返回值类型可以是父类返回类型的子类型(协变返回类型)。例如父类是Object get(),子类可以重写为String get()。
另一个常考的点是静态方法。静态方法可以被子类隐藏,但不能被重写,因为静态方法属于类而不是实例,在编译期就已经确定了调用目标。我在做题时看到这个选项,差点混淆,好在想起了“静态绑定”和“动态绑定”这两个概念:
- 静态方法调用在编译期就能确定,称为静态绑定。
- 实例方法调用需要根据运行时的实际对象类型来确定,称为动态绑定(虚方法表机制)。
2.3 JVM内存区域与对象创建过程
简答题里有一道是关于JVM运行时内存区域的划分以及对象头的组成。这类题在笔试中的出现频率很高,我从Java对象创建的过程切入来作答。
一个Java对象从new到回收,大致经历这几个区域:
- 堆:对象实例和数组的主要存储区域,几乎所有对象的创建都在这里发生。堆又分为新生代(Eden、Survivor from、Survivor to)和老年代。
- 虚拟机栈:每个线程私有一块栈,栈中存放栈帧。每调用一个方法,就会创建一个栈帧,栈帧里包含局部变量表、操作数栈、动态链接、方法返回地址。
- 方法区/元空间:存储类信息、常量、静态变量、即时编译器编译后的代码。在JDK 8之后,永久代被移除,改为元空间,使用本地内存。
- 程序计数器:当前线程执行字节码的行号指示器,分支、循环、跳转、异常恢复等都依赖它。
回答“对象创建过程”时,我按这个顺序展开:
- 类加载检查:虚拟机遇到new指令时,先检查这个类是否已被加载、解析、初始化。
- 分配内存:根据指针碰撞或空闲列表的方式,在堆中划分出一块确定大小的内存。
- 内存空间初始化为零值:这一步保证了对象的实例字段可以不赋初值就直接使用(局部变量不行)。
- 设置对象头:存储对象的运行时元数据,如哈希码、GC分代年龄、锁状态标志、类元数据指针等。
- 执行构造方法:将对象初始化到预期的状态。
这道题的加分点在于回答“对象在栈上分配的可能性”。虽然绝大多数对象分配在堆中,但JVM的逃逸分析技术会尝试把不逃逸的对象分配在栈上,随方法结束自动销毁,避免GC压力。能提到这一点,说明你不是单纯背八股,而是理解过JVM的实际优化。
2.4 类加载机制:双亲委派模型的答题要点
有一道单选考了双亲委派模型,问的是某个自定义类加载器在加载类时的查找顺序。
双亲委派模型的含义是:当一个类加载器收到类加载请求时,它不会自己尝试加载这个类,而是把请求委派给父类加载器去完成,每一层都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器(Bootstrap ClassLoader)中。只有在父类加载器反馈自己无法完成加载时,子加载器才尝试自己加载。
这样设计最重要的原因是安全:比如java.lang.String这个类,无论哪个类加载器想加载,最终都会委托给启动类加载器去加载,保证核心类库只加载一份,不会被用户自定义的恶意代码冒充。
笔试中常见的追问是“如何打破双亲委派模型”,常见思路是重写loadClass()方法而不去重写findClass()方法。因为loadClass()里实现了双亲委派的逻辑,自定义加载器通常是重写findClass()来完成具体的查找逻辑,而不是破坏委派过程。
3. 集合框架与并发源码:HashMap、线程池这类题怎么答出层次
3.1 HashMap底层结构与扩容机制
简答题里有一道非常典型的题目:描述HashMap从JDK 7到JDK 8的变化,以及put方法的执行流程。
JDK 7时代,HashMap的底层是数组加链表,链表插入使用头插法。在高并发场景下,多个线程同时扩容可能引发循环链表,导致get操作出现死循环。
JDK 8的改进有几个关键点:
- 链表改为尾插法,避免扩容时出现循环链表。
- 当链表长度超过8且数组长度大于等于64时,链表会转化为红黑树,把查找时间从O(n)优化到O(log n)。
- 扩容时不需要像JDK 7那样重新计算hash,只需要看原来的hash值新增的bit位是0还是1,为0则留在原位置,为1则移动到“原位置+旧容量”的位置。
这里我补充一下为什么转红黑树要链表长度大于8。根据泊松分布,在负载因子0.75的情况下,链表长度达到8的概率已经非常低(约千万分之一)。也就是说,只有当哈希分布出现严重问题时才会触发这个阈值,此时用红黑树来兜底。这个细节如果能在作答时提到,会显得你确实研究过源码而不只是背结论。
关于HashMap的初始化容量,笔试中还可能考到“给定初始容量16,扩容后容量是多少”这种题目。扩容是2倍,16变32,32变64,这个没啥好说的。真正有意思的是Map.EntryvsNode的区别,以及JDK 8之后红黑树的节点类型TreeNode是LinkedHashMap.Entry的子类,这影响了迭代时的一些行为。不过一般笔试不会考这么深。
3.2 ConcurrentHashMap的锁粒度演进
有一道多选题问到ConcurrentHashMap在JDK 7和JDK 8中的区别,选项包括锁机制、数据结构、并发度等。
JDK 7的ConcurrentHashMap使用分段锁,整个Map被分为16个Segment(默认并发度16),每个Segment是一把独立的ReentrantLock。不同的线程操作不同的Segment互不干扰,所以并发度上限是16。
JDK 8抛弃了分段锁,改为CAS加synchronized的方式。锁的粒度从Segment细化为单个桶(数组的每个下标位置)的头节点。这意味着只要两个线程操作的是不同桶中的元素,就可以真正并发执行,并发度远高于16。
JDK 8的put过程大概是:
- 如果table为空,执行
initTable()初始化。 - 如果根据hash定位的桶为空,通过
CAS尝试直接放入节点。 - 如果
ForwardingNode标记表明正在进行扩容,则当前线程协助完成扩容。 - 如果桶不为空,则
synchronized锁住桶的头节点,然后在链表或红黑树中执行插入。
我当时在回答中特别强调了一点:CAS只有一次成功的机会,失败就进入锁的流程。这种“先乐观后悲观”的并发策略,在JDK 8里处处可见,理解了这个思路,ConcurrentHashMap很多设计就好懂了。
3.3 线程池参数与执行流程
简答题里出现了描述ThreadPoolExecutor执行流程的题目,这是Java并发的经典考点。
核心参数就七个:
corePoolSize:核心线程数。maximumPoolSize:最大线程数。keepAliveTime:非核心线程空闲的最大存活时间。unit:存活时间单位。workQueue:任务队列。threadFactory:线程工厂。handler:拒绝策略。
执行流程可以浓缩成四个判断:
- 当前线程数是否小于核心线程数?小于则直接新建线程执行任务。
- 线程数已大于等于核心线程数,尝试把任务丢进工作队列,队列满了则进行下一步。
- 当前线程数是否小于最大线程数?小于则新建非核心线程执行任务。
- 线程数已达到最大,且队列已满,触发拒绝策略。
拒绝策略有四种:AbortPolicy(抛异常)、CallerRunsPolicy(调用者线程直接执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢弃队列中最老的,重新提交当前任务)。
笔试容易考的是:当核心线程数被突然调大时,会怎么处理?答案是线程池不会马上创建新的核心线程,而要等待新的任务到来时才会创建。ThreadPoolExecutor的prestartAllCoreThreads()方法可以提前启动所有核心线程,但这在实际项目中很少用。
我当时的经验是:遇到线程池相关的题,一定要把“任务是怎么一步步被处理的”这条脉络说清楚,中间不要跳过“队列满了”这个前提。很多同学一上来就写拒绝策略,忘了说明是“线程数达到最大值且队列已满”这个双重前提默认情况下线程池是什么行为。
3.4 手写单例的并发分析
编程题的旁边还有一道简答,给出了几种单例写法并要求指出线程安全问题。其中一种写法是这样的:
public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }这种懒汉式写法很明显是线程不安全的,两个线程同时判断instance == null后,可能各自创建一个实例。标准解法是加双重检查锁:
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关键字。它保证了两件事:一是可见性,二是禁止指令重排序。instance = new Singleton()在字节码层面不是一步操作,而是三步:
- 分配内存空间。
- 初始化对象。
- 将引用指向内存地址。
如果不加volatile,步骤2和3可能被CPU乱序执行,另一个线程可能拿到一个还没初始化完成的对象引用。这种“半初始化”问题很隐蔽,笔试时值得好好展开说明。
4. 数据库与框架题:从MySQL索引到MyBatis的细节
4.1 索引失效场景与SQL优化
笔试中涉及数据库的题目主要集中在MySQL索引上,尤其是索引失效的几种场景和SQL优化的基本思路。
常见的索引失效场景包括:
- 对索引列使用函数计算,如
WHERE YEAR(create_time) = 2020。 - 隐式类型转换,比如索引列是varchar,但查询条件是数字类型。
- 左模糊匹配,
LIKE '%java'导致无法使用索引。 - 联合索引不符合最左前缀原则。
- 查询条件中对索引列进行了表达式运算。
- 优化器判断全表扫描比走索引更高效时,也会放弃索引。
有一道题目设计的场景是:在(a, b, c)上建了联合索引,问哪些查询能用上索引。这个知识点不难,关键在于a = 1 and c = 3这种查询中,a能用索引,但c因为中间缺少b的条件,无法使用索引。很多同学会误以为只要是联合索引中的列就能用,忽略了最左前缀。
我当时的答题策略是:先摆出联合索引B+树的组织方式,说明索引的排序规则是“先按第一列排序,再按第二列排序……”,所以跳过了某一列,后面的列就失去了有序性,自然无法用于索引查找。这样说清楚了原理,就不需要死记硬背。
4.2 事务隔离级别与MVCC
有一道多选考的是事务隔离级别,题目给出了几种隔离级别,让选出“不会出现脏读”的选项。
MySQL的四种隔离级别是:
- 读未提交(Read Uncommitted):允许读取未提交的数据,脏读、不可重复读、幻读都可能发生。
- 读已提交(Read Committed):只能读到已提交的数据,解决了脏读,但不可重复读和幻读仍可能发生。
- 可重复读(Repeatable Read):同一事务内多次读取结果一致,解决了不可重复读,但幻读仍可能发生。MySQL的InnoDB在实现上通过间隙锁解决了幻读问题。
- 可串行化(Serializable):完全串行化,隔离级别最高,性能最低。
这道题不光是考概念,还考了一个细节:MySQL默认隔离级别是可重复读,而不是读已提交。这是Oracle的做法,两者不同,有的同学容易搞混。
MVCC(多版本并发控制)的原理其实可以简单理解成:每行记录有多个版本,不同事务看到不同的版本。它基于隐藏的DB_TRX_ID和DB_ROLL_PTR字段,以及undo log中的版本链来实现。快照读的时候,根据当前事务的read view来判断哪些版本可见。可重复读和读已提交的差别就在于read view的生成时机:
- 读已提交:每条查询语句都生成一个新的
read view。 - 可重复读:同一个事务内复用同一个
read view,所以读到的数据始终一致。
我当时在简答题中画了这个对比(用文字描述),明显感觉答题的深度上了一个档次。这也是我反复提醒自己的一个原则:笔试中回答原理类问题,尽量从底层机制说起,不要只列表面结论。
4.3 MyBatis的#{}与${}区别
框架相关的题目里,MyBatis几乎必考#{}和${}的区别。这题不难,但实操中极易踩坑。
#{}是预编译处理,MyBatis会把它替换成?占位符,然后通过PreparedStatement参数绑定来设置值,可以防止SQL注入。${}是字符串拼接,MyBatis直接把参数值拼接到SQL语句中,存在SQL注入风险。
既然${}有风险,为什么还要用?答案是有些场景下它是必须的,比如参数是表名、列名、排序字段时不支持预编译。如果你项目里用了MyBatis-Plus的动态条件查询,底层构造排序字段时也需要用拼接的方式。
需要注意的是:即便是表名、列名这类需要用${}的场景,也必须采用白名单校验,避免用户输入直接拼进SQL。比如排序字段,可以在代码里做一个枚举校验,只允许asc或desc,其他一律拒绝。这个经验是我在实际项目里踩过坑之后总结出来的,笔试里如果被追问“怎么安全地使用${}”,能答出白名单校验,会是很好的加分项。
4.4 Redis常用数据结构选型
笔试里还有一道关于Redis的题,考察的是各数据结构的适用场景。题目大概给出了“缓存用户信息”“统计UV”“排行榜”这几个场景,让你选择对应的数据结构。
- 缓存用户信息:可以用String(JSON序列化)或Hash(字段级别读写)。
- 统计UV:用HyperLogLog,虽然有一定的误差率,但内存占用极小。
- 排行榜:用ZSet,通过分数排序,天然支持按名次范围获取。
MySQL和Redis的经典组合类是常考重点。Redis作为缓存,需要考虑缓存穿透、缓存击穿、缓存雪崩的处理。缓存穿透是指查询一个必定不存在的数据,导致请求直达数据库;解决思路是布隆过滤器或者缓存空值。缓存击穿是指某个热点key突然失效,大量请求同时落到数据库;解决思路是加互斥锁,或者设置逻辑过期时间。缓存雪崩是指大量key同时失效,解决思路是给过期时间加随机值,避免同一时刻大面积失效。
笔试时候能把这些点都答上,数据库这部分的分数基本就稳了。别只答“用Redis做缓存”就完了,得说出怎么用、遇到问题怎么办。
5. 手写算法与场景设计题:笔试中最考验应变的部分
5.1 排序算法的手写套路
浩鲸科技的编程题中没有要求写快排或归并,而是出了相对简单但更贴近业务的题目。不过我还是建议把冒泡、选择、插入、快排这四个基础排序都准备到手写一遍的程度。万一题目就是让手写排序,至少不能在这里翻车。
一个常见的编程题是:给定一个整数数组,要求将所有的偶数排在前面,奇数排在后面,并且保持相对顺序不变。
这道题其实就是“稳定分区”问题。最简单的实现方式就是用一个新数组做两次遍历:第一次把偶数放进结果数组,第二次把奇数放进去。这样时间复杂度是O(n),空间复杂度是O(n)。
如果要求原地操作,思路就和冒泡排序有点像,从后往前把相邻的奇偶交换到“不对”的位置,但是要保持稳定就需要多一些操作。我当时的做法是直接用新数组,简单又不容易出错。
5.2 字符串处理题的边界
还有一道编程题是字符串相关,印象中是把字符串中的连续空格替换为单个空格。这类题看起来很基础,但边界条件很多。常见的边界包括:
- 字符串开头、结尾有空格。
- 连续多个空格。
- 字符串为空或长度为0。
- 字符串中只有空格。
我当时用了一个比较稳的方法:先去掉首尾空格,然后用正则或遍历处理中间的多余空格。笔试环境不一定允许网络搜索,建议自己熟悉Java中StringBuilder的用法,因为字符串拼接在笔试中经常用到,而StringBuilder是性能最优且最常用的选择。
5.3 场景设计题的答题框架
编程题之外的简答里,有一道场景设计类型的题目:假设有千万级用户量的系统,如何设计一个用户登录状态校验方案。
我当时的答题思路分了三层:
第一层,用Redis存储token。用户登录成功后生成一个token,存到Redis里并设置过期时间,后端每次请求都从Header中取出token到Redis中校验。这是最常见、也是最容易想到的方案。
第二层,考虑分布式会话的问题。单机Tomcat的Session无法在多节点共享,直接把Session放到Redis,或者使用JWT这类无状态token来避免Session共享问题。
第三层,考虑安全与性能的平衡。token的过期时间、续期策略、token失效后的处理方式,都是需要明确设计的。比如用户长时间不操作时,可以用滑动过期策略,每次请求时刷新过期时间。
这个答题框架的好处是:先给一个能跑的方案,再指出这个方案的缺陷,然后逐层优化。这种“演进式回答”在任何场景设计题中都非常通用。面试官或判卷人看到的不只是你知道某个技术,而是你具备架构式的思考习惯。
6. 复盘总结:投递笔试题时我做对了哪些准备
浩鲸科技的这场笔试考完,我整体的感觉是:题都不算难,但真要全部答好,靠的是平时的积累,不是临阵磨枪。复盘之后,我总结了三个值得分享的经验。
6.1 高频考点的优先级排序
如果你的准备时间有限,我建议按这个优先级来复习Java笔试题:
- 集合源码:HashMap、ConcurrentHashMap、ArrayList扩容机制。
- Java基础语法细节:String常量池、重载重写、装箱拆箱、异常处理。
- 并发基础:synchronized与ReentrantLock、线程池执行流程、CAS与volatile。
- JVM内存区域与类加载机制。
- MySQL索引与事务。
- Spring/MyBatis/Redis使用层面的常见问题。
把前三条弄扎实,基本能覆盖大多数公司的Java笔试核心考察范围。
6.2 答题时的几个细节习惯
- 多选不确定就少选,尤其在“少选不得分”的规则下,保住确定项比冲全对更重要。
- 简答不要只写结论,把你得出这个结论的推导过程和底层原理也写出来,这是区分“背过”和“理解”的关键。
- 编程题先想清楚边界条件再动手,写完最好在心里跑一遍测试用例。
- 遇到不会的题先跳过,把确定能拿到的分数拿到手,再回来研究不会的题。
6.3 笔试之后的衔接
笔试之后紧接着的一般是技术面。浩鲸科技这类公司,笔试中出现的知识点大概率在面试中还会以追问的形式出现。比如你笔试答了ConcurrentHashMap的JDK 8实现,面试官很可能追问:为什么JDK 8要用synchronized而不是ReentrantLock?这个话题如果能在笔试后提前想清楚,面试时就不会被问住了。
我当时还做了一件事:把笔试中所有没把握的题目重新整理了一遍,对照源码和官方文档,把每个知识点吃透。这样做的好处是,即使笔试分数不理想,面试时还能通过深入交流展示自己的真实水平。
最后再提醒一句:笔试刷题网站上的模拟题库值得反复做。浩鲸科技的题目并不完全是原创的,很多考点在牛客、力扣的题库中都能看到类似版本。多刷几遍,熟悉常见考法和答题节奏,真正上考场时会从容很多。