每年秋招的Java笔试,风格差异大到像两个物种。有的公司喜欢海量八股选择,有的上来就手撕hard题,还有些玩情景题。贝壳找房2024年秋招Java工程师第一批笔试,我踩完坑之后觉得非常值得复盘,整体属于“基础面广、算法够用、Java考得深”的路子,今天就把考了什么、怎么准备、踩了哪些坑一次说清楚。
这篇内容适合两类人:马上要参加后续批次笔试的应届生,以及准备跳槽、想摸底大厂笔试难度的Java开发。我会把笔试中涉及的Java核心考点、算法思路、计算机基础题都拆开讲,不只是报答案,重点说清楚“为什么这么答”和“我当时哪里想岔了”。
1. 笔试整体情况与题型分布
贝壳这批笔试是线上双机位,总时长90分钟,题量不算小。题型分成三块:单选多选混排的选择题、2道手撕代码题、若干简答/改错题。选择题侧重Java基础、并发编程、JVM、Spring和MySQL,代码题走的是经典算法路子,没有特别偏门的ACM题型。
先说整体感受:贝壳的笔试不像某些互联网大厂那样疯狂堆算法题量,它更在意基础扎实度。选择题涉及的知识面很广,我在刷题时复习过的Java内存模型、HashMap原理、线程池参数、Spring Bean生命周期几乎都考到了。而代码题难度中等偏上,2道题从读题到写完自测,我大概剩了20分钟做选择题中的疑难题和检查,时间分配上需要提前心里有数。
这里放一下我记忆中的题型分布参考:
| 模块 | 题量 | 分值占比 | 主要考查方向 |
|---|---|---|---|
| 选择题(单选+多选) | 约20题 | 40% | Java基础、并发、JVM、Spring、MySQL、Redis |
| 简答/改错题 | 2题 | 20% | 代码阅读、设计思路、异常排查 |
| 算法编程题 | 2题 | 40% | 数据结构、动态规划、字符串处理、排序 |
提示:线上笔试的三方监控一般都开着,切屏会警告,超过一定次数直接算作弊。我全程老老实实只开着一个本地代码编辑器窗口,也建议你用本地IDE练习,提前适应这种“本地写题+网页提交”的节奏。
时间分配上我建议:拿到卷子先花一分钟扫一遍所有题目,心里对代码题难度有个预判,优先做自己有把握的。我的策略是先花10到15分钟快速过掉选择题里不涉及计算和代码推导的送分题,然后集中40分钟做算法题,剩下的时间回头啃选择题中的多选和疑难项。实际上多选的坑比单选多得多,多选漏选、错选都不得分,确实需要留出时间。
2. Java核心考点拆解:贝壳笔试偏爱哪些八股
这次笔试的Java部分,明显能看出出题人对“并发”和“容器”两个板块有执念。我回忆了一下,选择题里涉及HashMap的至少出了三题,线程池出了两题,JVM内存模型和垃圾回收各一题。难怪每年Java面试八股文永远绕不开HashMap,不是没道理的。
2.1 HashMap原理:不是只背“数组加链表”就完事
选择题里面有一道是这样的:JDK 8的HashMap在什么条件下链表转红黑树?答案是链表长度大于等于8且数组长度大于等于64。很多人只记得8,忽略了数组长度64的前提,我还在评论区见过有人争这个细节,其实源码注释写得很清楚,TREEIFY_THRESHOLD只是触发树化的第一个条件,如果数组长度不到64,是会先resize而不是直接转树。
还有一道问put操作的完整流程:计算hash->定位桶下标->判断是否空桶->判断是否树节点->遍历链表查找key->不存在则尾插->判断是否达到树化阈值->判断是否扩容。这个流程背下来不难,难的是理解为什么用高低位异或扰动函数。实际上源码里(h = key.hashCode()) ^ (h >>> 16)这一步,是把哈希值的高16位和低16位做异或,目的是让高位信息也能参与低位运算,降低哈希碰撞概率。当数组长度比较小时,能参与取模运算的只有低几位,如果没有扰动,碰撞概率会明显上升。
扩容相关的那道选择题也很典型:默认加载因子0.75,为什么不是0.5或者1?0.5浪费内存空间,1则碰撞概率太高,0.75是空间和时间的一个折中,这是从统计学上泊松分布推导出来的。这里提醒一下准备后续笔试的朋友,不只要记住默认值,还要能说出“为什么是这个值”,贝壳笔试的很多选择题就是在这个“为什么”上挖坑。
2.2 线程池:状态机、核心参数与拒绝策略
线程池的考题更偏向实际运用。有一道选择题给了四个参数配置,问你哪种配置可能导致任务排队时间过长甚至OOM。核心点就是:如果核心线程数太小,队列又用得是无界队列,比如LinkedBlockingQueue默认构造,那么当任务量陡增时,线程数永远只停留在corePoolSize,因为核心线程没满就不会创建新线程,任务全堆积在队列里,内存就容易被撑爆。这里我还想多写一点:我把newFixedThreadPool为什么不推荐用也和这道题联系到一起了,因为这个线程池用的就是无界队列,即使设置了很大的maximumPoolSize也形同虚设,不会触发拒绝策略,容易导致无限排队。
关于线程池的执行顺序,我喜欢用一个生活化类比来记:线程池就像一个饭店,核心线程数是正式员工,队列是等待区,最大线程数是临时工上限,拒绝策略是实在接待不了时的处理方式。来一个客人先看正式员工有没有空,有就接待;没有就引导到等待区排队;等待区满了才考虑招临时工,这里有个细节,招临时工的顺序是在等待区满了之后才会进行的;如果临时工也满了,才启用拒绝策略。笔试里有一道判断题就考了这个顺序:新任务到达时先判断线程数是否小于核心线程数,不是先丢队列,顺序反了就直接答错。
拒绝策略这块,AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy它们各自的适用场景也要清楚。我记得有一道多选题就是给场景选策略:如果任务绝对不能丢,应该选什么?答案是CallerRunsPolicy,因为它在拒绝时会直接在调用者线程里执行任务,相当于把压力回传给调用方,而不是静默丢弃。而AbortPolicy是默认策略,直接抛异常;DiscardPolicy和DiscardOldestPolicy都是丢任务的,前者丢新的,后者丢最老的,要看场景选。
2.3 JVM内存区域与垃圾回收:画图记忆比死记硬背强
JVM相关考题是典型的“画图就能得分”的题目。选择题问的是:哪个区域不会发生OutOfMemoryError?答案是程序计数器。理由也很简单:程序计数器是线程私有的,它的空间需求在类加载时就已经确定了,用来存下一条字节码指令的地址,大小恒定,不会随着运行时数据增长而变化。我在复习时把运行时数据区画了张图:线程共享的堆和方法区,线程私有的虚拟机栈、本地方法栈、程序计数器,每次看图比死记硬背快得多。
垃圾回收的题问的是CMS的回收阶段。CMS全称Concurrent Mark Sweep,它的回收过程分为初始标记、并发标记、重新标记、并发清理四个阶段。其中初始标记和重新标记需要STW,并发标记和并发清理跟用户线程并发执行。题目问哪个阶段会STW,很多人的误区是以为CMS全程并发,实际上初始标记和重新标记都会暂停用户线程。CMS在Java 9之后被标记为废弃,很多企业已经切换到G1了,但笔试考老知识点仍然频繁。
注意:看题时要看清楚问的是“哪个阶段会STW”还是“哪个阶段不会STW”。我在做这道题时第一眼差点选错,这些词汇陷阱在笔试中经常出现,建议每道选择题把题干关键词圈出来再选。
3. 手撕代码题:2道经典题的思路与Java实现
贝壳的算法题没有特别劝退,属于“认真准备过笔试就能写出正解”的难度。第一道考的是分组排序的变体,第二道是动态规划。两道题我都在本地IDE里写完并自测了几个用例才提交,功夫在平时,建议你平时练习时也养成写完手写几个测试用例的习惯。
3.1 字符串分组排序变体
题目大意是:给定一个字符串数组,将字母异位词(由相同字符重排形成的词)分到同一组,每组内部按字典序排序,最终按每组第一个元素排序输出。这道题考察两个点:一是如何高效判断两个字符串是异位词,二是如何保持分组内部和分组的整体顺序。
判断异位词最常用两种方案:一是将字符串转成字符数组后排序,排序结果相同的就归为一组,时间复杂度O(nlogn),胜在实现简单;二是用长度为26的int数组记录每个字母出现次数,将数组转成字符串作为key,时间复杂度O(n),更优但代码略长。实际写题时我采用了第二种,因为笔试环境里的输入规模没有明说,稳妥起见选择线性复杂度方案。
实现思路:遍历字符串数组,对每个字符串统计字符频率,把统计结果转成key存入HashMap,value是对应的字符串列表。关键点在于如何把int数组转成稳定的key,我用的方案是拼接成“1#0#3#...”这种带分隔符的字符串,防止出现歧义,比如[1,23]和[12,3]这种边界情况。分组完成后,对每个组内列表排序,再对所有组的首个元素排序确定组的输出顺序。
这题相对基础,不过它考察了字符串处理、哈希表、排序的综合运用,要求对类库API熟悉。平时不常写字符串处理题的话,可能在中途卡在“如何优雅地把频率数组转成key”上。
3.2 编辑距离:动态规划的状态定义与转移方程
第二道题是编辑距离,原题描述是给定两个单词word1和word2,计算将word1转换成word2所需的最少操作数,可以插入一个字符、删除一个字符、替换一个字符。这是动态规划里的入门经典题,也好多年没在笔试里遇见了,看到题目时还愣了一下。
状态定义比较直观,dp[i][j]表示word1的前i个字符转换成word2的前j个字符需要的最少操作数。初始化也比较简单:dp[0][j]=j(不断的插入),dp[i][0]=i(不断的删除)。转移方程按最后一个字符是否相等分两种情况:
- word1[i-1] == word2[j-1],当前字符不用操作,直接
dp[i][j] = dp[i-1][j-1]; - 不相等时,取三种操作的最小值加1:
- 插入:
dp[i][j-1],在word1第i个字符后插入一个与word2[j-1]相等的字符; - 删除:
dp[i-1][j],删除word1第i个字符; - 替换:
dp[i-1][j-1],将word1第i个字符替换成word2[j-1]。
- 插入:
两层循环填完整个二维数组后,dp[m][n]就是答案。注意这里两个字符串可能都为空,边界条件要处理干净。我写了大约30分钟的Java代码,含注释,你自己写的时候建议先从状态定义开始理清思路再动手。
3.3 笔试中的算法取舍与测试意识
代码题不会只考“写对”,还会考察“在有限时间和内存下的取舍”。第一题我用HashMap统计频率,空间复杂度O(n),每个key最长不超过26个字符,可以接受。第二题用二维数组存状态,如果两个字符串长度都到1000,二维数组就是100万级别,没问题;但如果长度到一万,这题就应该优化成滚动数组了。幸好笔试时发现长度范围控制在500以内,二维数组直接过。
我当时写完每道题都手动构造了三个测试用例:正常情况、边界情况、特殊输入。比如编辑距离那道,我测试了word1为空、word2为空、两字符串完全相同这三种边界。笔试时因为测试用例能加分,花两分钟做自测完全值得,总比提交后开始担心边界没过要强。
4. 计算机基础考察:数据库、Redis与网络知识
选择题中数据库和Redis的题量也不少,这点和贝壳的业务场景挺匹配,房产信息、用户关系、搜索推荐这些业务都重度依赖存储层。笔试把MySQL和Redis放在一起考,看得出对后端基本功的要求不是停留在“会用CRUD”的层面。
4.1 MySQL索引失效场景与事务隔离级别
数据库部分有一道选择题问哪些情况会索引失效,选项包括隐式类型转换、对索引列使用函数、like语句前缀通配符、or连接非索引列。这些都是高频考点,我在面试中也常碰到。具体说,隐式类型转换会让MySQL忍不住调用CAST函数,导致索引列上套了函数,索引就失效了;where name like '%张'这种前缀模糊查询无法走索引,因为B+树的索引顺序是从左往右匹配的,前缀不确定就定位不了;where a = 1 or b = 2如果b没有索引,优化器可能会选择全表扫描,因为它要同时满足两边的条件,没法走一个索引把所有结果都捞出来。
事务隔离级别也考了一道填空题加选择题组合:MySQL默认的隔离级别是Repeatable Read(可重复读),这一点和标准SQL默认的Read Committed不同。可重复读能解决脏读和不可重复读,但没法完全解决幻读,InnoDB通过间隙锁(Next-Key Lock)在部分场景下解决了幻读,但不代表所有场景都彻底避免了。我回忆笔试中的那道选择题,它问的是“脏读、不可重复读、幻读分别在哪个隔离级别下被解决”,很多人背了隔离级别名称,但没区分清楚不可重复读和幻读的差异:不可重复读针对同一行数据多次读取结果不一致,幻读针对的是新增或删除导致的结果集数量变化。
4.2 缓存三大问题:穿透、击穿、雪崩
Redis部分的题比较常规,考到了缓存穿透、缓存击穿、缓存雪崩的区别和应对方案。我猜是因为贝壳找房的房源地库查询对缓存依赖度很高,线上对缓存稳定性的要求自然不低。我把三者的区别整理成了一张表,后续去其他公司笔试也用得上:
| 问题 | 场景 | 应对方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据,每次都落到数据库 | 布隆过滤器拦截、缓存空值(短TTL) |
| 缓存击穿 | 热点key失效瞬间,大量请求打到数据库 | 互斥锁重建缓存、逻辑过期、热点key永不过期 |
| 缓存雪崩 | 大量key同时过期或Redis宕机 | 过期时间加随机值、集群高可用、限流降级 |
还有一道考察Redis数据结构的题,问的是实现排行榜用什么结构。答案是ZSet(有序集合),因为ZSet底层是跳表加哈希表,支持按score排序和范围查询,时间复杂度O(logn)级别,比在内存里自己维护一个有序列表高效得多。贝壳这种业务里房源价格排序、热门小区排行,确实都是ZSet的典型应用场景。
4.3 TCP与HTTP的送分题背后还有深层考点
网络部分的题相对简单一些:TCP三次握手为什么是三次而不是两次?TCP和UDP的区别是什么?HTTP和HTTPS的端口、安全性差异?不过后面跟了一道稍微进阶的:浏览器的同源策略和HTTP跨域问题。Java后端虽然不直接处理浏览器CORS,但Spring的CORS配置、网关层的跨域处理都是实际开发中绕不开的,笔试考察这个说明出题人比较看重工程实践。做这类题时别只背概念,适当结合项目经验讲更好,毕竟笔试题的题干里经常有“开发中遇到……怎么排查”这种场景化描述。
5. Spring与工程实践:贝壳笔试里的业务视角
Spring相关的题目在选择题里大概有五道左右,比重不小。涉及的内容包括Bean生命周期、自动装配流程、事务传播行为、Spring Boot自动配置原理。和我预想的不同,这些题不像“八股背诵”那么死板,更像是给一个具体场景让你判断容器或事务会怎么走。
5.1 Spring Bean生命周期与循环依赖
有一道题给了一段代码,包含两个互相依赖的Bean,然后问在默认单例模式下能不能正常启动。答案是能,因为Spring三级缓存机制解决了单例Bean的循环依赖问题。三级缓存分别是:一级缓存存放完整实例,二级缓存存放早期暴露的未完全初始化的Bean,三级缓存存放代理工厂。Bean在实例化后、属性填充前就会把ObjectFactory放入三级缓存,这样A依赖B时,B在创建过程中依赖A就能从三级缓存中拿到早期引用,避免死循环。
但这道题的扩展选项很有迷惑性:如果其中一个Bean是@Async代理,循环依赖还能不能解决?答案在某些情况下是不能。因为代理对象的创建时机会影响循环依赖的解决路径,这类题如果没有真正看过三级缓存源码,很容易做错。我复习时把三级缓存、Bean的实例化到初始化的整个过程都手画了一遍,才对这类变体题有把握。
5.2 Spring事务传播行为:REQUIRED和REQUIRES_NEW的区别
事务传播行为考了两道题。一道是经典的自调用问题:一个类的内部方法通过this调另一个带@Transactional的方法,事务会不会生效?答案是不会。因为事务是通过AOP代理实现的,内部调用走的是this本身,没有经过代理对象,事务注解自然凉了。解决方法是注入自身代理,或者把方法拆到另一个Bean中。
另一道则是问REQUIRED和REQUIRES_NEW在嵌套调用下的行为。REQUIRED表示如果当前存在事务就加入,不存在就新建;REQUIRES_NEW则是无论当前有没有事务都挂起当前事务,新建一个独立事务。选择题里的坑在于“方法A调用方法B,A抛异常,B在REQUIRES_NEW下已经提交,最终数据结果如何”。答案是A回滚、B不回滚。如果没理解透传播行为,看到A抛异常就容易脑补成“都回滚”。这类题其实在笔试中出现率很高,做对了能拉开差距,毕竟很多人对事务的实际控制边界不够敏感。
5.3 让八股变成工程直觉:我的复盘方法
笔试结束我发现一个规律:贝壳的Spring题基本不是单独考某个注解的用途,而是给一个开发场景,让你判断运行结果。这就意味着死记硬背不够,得有“运行时视角”。我复盘时特意写了一套模拟场景——线上接口偶尔报错,日志里看到事务回滚、缓存数据不一致,然后逼自己从Bean生命周期、事务传播、Redis过期策略的角度去推断原因。这个习惯帮我理清了知识点之间的联系,也帮我在后续面试中把“背过的八股”讲成了“踩过的坑”。
6. 笔试备考策略:一个月滚出高频考点的完整路线
如果你也准备冲秋招后续批次或者其他公司的Java岗笔试,我的核心建议是:按“算法高频题、Java八股、计算机基础、框架原理”四条线并行刷,不要先啃完一本大部头再开始做真题,时间根本不够。
我备考用的资料其实很固定:算法刷题用LeetCode热门100题加剑指Offer,Java看《Java并发编程的艺术》里的关键章节,JVM看《深入理解Java虚拟机》前几章加在线总结,Spring直接看源码解析类文章加自己打断点验证。每看完一块知识,我会用一小时把相关知识点整理成自己的话,逼自己输出,避免“看书全会、做题全废”。
时间安排上,我建议如果只有四周,可以这样分:第一周集中攻算法,把数组、链表、树、动态规划这几类基础题过一遍;第二周主攻Java基础加并发,每天做10道选择题加2道手写代码;第三周看MySQL、Redis、操作系统和网络,配合项目思考底层原理;第四周整卷模考,掐时间按真实笔试环境做2到3套模拟题,练节奏和心态。我这次贝壳笔试的准备过程,其实也基本走了这个节奏,只是当时把更多时间给了算法,导致Spring部分有几道题做起来有点犹豫,复盘后才补上。
笔试当天有一点比较重要:提前调试好电脑、浏览器和摄像头。贝壳笔试用的是国内常见的在线考试平台,有模拟测试入口,我提前一晚完整跑通了流程。别因为网络波动或者摄像头权限没开影响进入考试的心态,笔试已经开始扣的一分钟就是浪费。另一个细节是用本地IDE写代码时注意类名和主方法签名,笔试平台要求提交的代码通常是纯Java类,不是整个工程,命名不规范可能导致编译失败。
7. 复盘总结:从贝壳笔试反推训练重点
考完复盘时,我把自己定位成一个“基础还有欠缺但方向对了”的候选人。贝壳这批笔试给我最大的启示是:Java后端笔试真正拉开差距的不是算法难题,而是对并发和JVM底层机制的熟练掌握程度。选择题里最让我拿不准的,是那些“源码里默认参数为什么这么设计”的变体题,面熟但没深究就容易错。
一个比较深刻的感受是,贝壳的笔试题目整体偏向“业务复杂度不高但工程素养要求不低”的风格。它的算法题不偏难怪,但选择题的考察角度比很多公司更细,尤其是HashMap、线程池、Redis这三种高频组件的细节。如果你平时只是会用这些API而不看底层原理,做起来确实会比较吃力。
我也发现自己还有几个明确短板要补:Spring事务自调用问题的底层AOP原理我虽然知道答案,但论述时不够严谨;缓存雪崩和击穿的代码级解决方案我没有完整实现过,只是背了答案。后续我会针对每个盲区写一篇带代码的Demo再整理成笔记,把知识补扎实。秋招笔试只是第一关,把每一次笔试都当成查漏补缺的机会,后面的面试才有底气。
最后分享一个对我来说很管用的方法:每次笔试完,不管你发挥如何,立刻花半小时把错题和不确定的题记录下来,哪怕只是草稿纸拍个照,然后当天或第二天对照资料把每一个盲点弄清楚。这次贝壳笔试结束后,我重新整理了HashMap扩容和线程池拒绝策略的笔记,顺手把编辑距离的滚动数组优化也写了一遍。复习的黄金时间就是考后的48小时,千万别等全部笔试结束再统一复盘,那时候细节早就忘得差不多了。