贝壳Java笔试题复盘:从基础到分布式系统设计全解析

发布时间:2026/9/1 12:28:34
贝壳Java笔试题复盘:从基础到分布式系统设计全解析 这套卷子我在准备春招的时候完整复盘过好几遍。贝壳找房的Java笔试卷和市面上大多数互联网公司的卷子不太一样它的技术栈重心非常鲜明房产交易链路长、业务状态多、房源数据并发读写的压力大所以考的都是“你能不能撑住一套高复杂度的业务系统”这种问题而不是让你炫技写一段花哨算法。2023年春招这套Java工程师笔试卷2题型覆盖面很扎实我把每个模块的考察逻辑、我做题时的思路、以及整理出来的扩展知识点全部拆在这里。准备春招的同学不管目标是不是贝壳这套卷子背后的考点体系都值得认真过一遍。1. 题型结构与考察重心贝壳这场笔试到底想筛选什么人1.1 整张卷子的模块划分我把整张卷子回看了一遍大致可以分成五个模块Java语言基础、集合与并发、Spring与微服务、数据库与缓存、以及一道综合性系统设计题。每个模块的题目数量和难度都不是平均分配的。语言基础题偏多但绝大多数是送分题集合与并发次之但挖得比较深数据库与缓存是重头戏设计题单独占一题且直接影响综合评价。这个结构其实反映了贝壳这类业务型公司的真实用人逻辑语言基础是底线集合并发是日常开发的核心能力数据库与缓存决定了你能不能在高流量场景下写好代码而系统设计题是区分“会写代码”和“能独立扛业务”的人的分水岭。1.2 与其他公司笔试卷的横向对比之前我也刷过一些纯互联网公司的笔试卷有些卷子把大量分值压在算法题上比如动态规划、贪心、复杂图论两道写不出来基本就挂了。但贝壳这套卷子整体算法难度属于中等偏上不会刻意出偏题怪题反而更看重工程基础。题目里出现的并发场景、缓存失效、事务边界几乎都能在贝壳实际的房源检索、看房预约、交易单流转场景中找到原型。这给我的启发是准备这类公司笔试不能只顾着刷LeetCode还要把SSM/Spring Boot、MySQL调优、Redis缓存这些在实际业务里高频使用的技术整理成自己的知识体系。2. Java语言基础送分题里的“陷阱层”与边界问题2.1 字符串与包装类考的不是语法是JVM细节第一类题是看起来人畜无害的送分题但里面藏着JVM层面的细节。比如原卷里有一道很经典的题String str new String(abc)这行代码创建了几个对象很多人脱口而出“一个”正确答案是两个——一个在堆里的String对象一个在字符串常量池里的“abc”字面量对象。如果常量池中已存在“abc”则只创建一个堆对象。我当时做这道题的时候专门在草稿纸上推了一遍内存布局最后还是选了“分情况讨论”。这种题考的不是会不会背答案而是你能不能把字符串常量池、堆、栈之间的关系说清楚。还有一道Integer缓存的题问Integer a 127; Integer b 127; a b的结果以及换成128会怎样。这个考点本质是Integer内部缓存区间[-128, 127]在这个范围内会直接返回缓存对象超出范围则new新对象所以用比较就会出现“同值不同对象”的情况。实际开发中我一般建议用equals比较包装类别用去赌缓存区间。Integer a 127; Integer b 127; System.out.println(a b); // true走缓存 Integer c 128; Integer d 128; System.out.println(c d); // false超出缓存区间这类题在房产交易系统里的意义是房源信息、带看记录、价格数值往往以包装类形式在RPC层传输一不留神就会在比较逻辑上埋坑。面向对象编程Java的核心之一就是类型系统这种细节值得反复琢磨。2.2 异常体系受检异常与运行时异常的分寸这套卷子有一道异常设计题业务校验不通过时自定义异常应该继承Exception还是RuntimeException我印象里不少同学选了Exception理由是“受检异常强制调用方处理更安全”。但放在Spring Boot工程里正确答案是RuntimeException。原因要落到Spring事务机制上Spring默认只在抛出RuntimeException或Error时回滚事务如果抛的是受检异常且没有手动声明Transactional(rollbackFor Exception.class)事务不会回滚数据就会停留在半完成状态。我在实际项目里就踩过一次状态流转失败的异常继承了Exception结果订单状态改了但明细没插进去问题排查到凌晨才发现是事务没回滚。提示自定义业务异常统一继承RuntimeException配合ControllerAdvice全局异常处理在工程上是更省心的做法。2.3 枚举、Lambda、泛型看起来用到但总容易写错枚举在笔试里不是只考“定义几个常量”而是考能不能用枚举做状态机。贝壳的业务里房源状态、订单状态都有大量流转逻辑用枚举管理状态比散落一堆字符串常量健壮得多。原卷中有一道题是给房源上下架状态建模我给出的方案是枚举里放状态码和描述再写一个nextStatus方法来处理合法流转。public enum HouseStatus { OFF_SHELF(0, 已下架), ON_SHELF(1, 已上架), LOCKED(2, 已锁定); private final int code; private final String desc; HouseStatus(int code, String desc) { this.code code; this.desc desc; } }Lambda和函数式接口也考了一道核心是list.stream().filter().map().collect()链式调用以及方法引用写法。泛型考了擦除机制ListString和ListInteger在运行时是同一个Class所以不能用list instanceof ListString这种写法要么用instanceof List?要么通过元素类型做判断。这些知识点单独看都不难但组合在一起确实能区分出“背过八股文”和“真写过代码”的人。3. 集合框架与并发编程房产交易系统里最硬的两块基本功3.1 HashMap到底考到什么深度才算过关HashMap在Java面试里被问到的概率接近百分之百这套卷子也不例外。但它的考法不是“HashMap的底层结构是什么”这种教科书题而是从使用场景出发一层层追问。第一层HashMap的底层结构数组链表红黑树JDK 8在链表长度超过8且数组容量大于等于64时链表转红黑树为什么转红黑树因为链表查找是O(n)红黑树是O(log n)。为什么临界值是8这是泊松分布下的一个工程权衡源码注释里给出的是负载因子0.75时链表长度达到8的概率已经非常低。第二层扩容机制。默认容量16负载因子0.75当size超过容量 * 负载因子时触发扩容容量翻倍。JDK 8在扩容时引入了尾插法避免了JDK 7头插法在并发场景下可能出现环的问题。所以即使HashMap不是线程安全的JDK 8的修改也降低了并发扩容时的破坏概率。第三层改写equals后为什么必须改写hashCode。两个对象equals相等时hashCode必须相等否则HashMap在查找时会定位到不同的桶导致containsKey失效。这道题我答的时候补了一个实际场景房源对象作为key放入Map时如果只用房源ID做equals但hashCode用了所有字段就会频繁出现“找不到key”的诡异问题。3.2 线程安全集合的演进逻辑下面一道是集合并发安全的选型题。Hashtable为什么会被淘汰因为它把整张表锁住所有读写串行化并发度太低。Collections.synchronizedMap只是在方法级别加synchronized本质也是一把大锁。ConcurrentHashMap是标准答案但JDK 7和JDK 8的实现差异要说清楚。JDK 7用的是分段锁把数据分成一段一段默认16段锁粒度是段级别。JDK 8抛弃了分段锁改用CAS synchronized锁定数组中的每个桶头节点锁粒度更细并发度更高。源码里putVal方法先用CAS尝试插入空桶如果桶不为空再锁住头节点。我在做这道题时顺手补了一句读多写少的缓存场景里CopyOnWriteArrayList和CopyOnWriteArraySet也值得考虑它们的写操作会复制底层数组适合读远多于写的场景比如黑白名单、系统配置列表但不适合写频繁的库存类数据。3.3 线程池与锁并发题的答题框架线程池几乎是必考项。参数要能完整背出来corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime空闲线程存活时间、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。更重要的是执行流程新任务进来先判断核心线程是否已满没满直接创建核心线程执行满了就进阻塞队列队列也满了再创建非核心线程直到最大线程数达到最大线程数后触发拒绝策略。四种拒绝策略里AbortPolicy直接抛异常CallerRunsPolicy让调用线程自己执行DiscardPolicy和DiscardOldestPolicy会静默丢弃实际项目中我一般用CallerRunsPolicy做降级至少任务不会丢。volatile和synchronized也考了一题。volatile保证可见性和有序性但不保证原子性适合状态标志位synchronized是互斥锁能同时保证可见性、有序性和原子性。这里最好再补一句JMM的知识说明线程操作共享变量时先拷贝到工作内存volatile通过内存屏障强制刷新主内存这就是为什么多线程环境下不加volatile会出现“读不到最新值”的情况。并发场景题我额外补充一个高频考法多个线程同时扣减一个库存变量怎么保证不超卖除了加锁也可以用AtomicInteger的CAS自旋或者LongAdder在极高并发下减少竞争。这套卷子虽然没有直接考这三者对比但我在复盘时默认把它当成了并发模块的加分内容。提示写线程池相关的编程题时建议把拒绝策略线程工厂都配上不要只传一个线程数量这样能展示你对线程池完整生命周期的理解。4. Spring Boot与微服务房源系统的框架层考点4.1 IOC、AOP在笔试里的落地考法Spring的IOC不是只考“控制反转是什么”而是考你对Bean生命周期的熟悉程度。执行顺序大致是实例化 - 属性填充 - Aware接口回调 - BeanPostProcessor前置处理 - 初始化方法 - BeanPostProcessor后置处理。AOP代理就在BeanPostProcessor后置处理这个阶段生成所以一个Bean最终放进容器里的可能是代理对象。AOP最常见的考法是动态代理的两种方式JDK动态代理要求目标类实现接口基于反射生成代理类CGLIB则直接生成目标类的子类不要求接口。Spring Boot 2.x之后默认使用CGLIB代理很多生产问题都出在这里——如果某个类被代理后内部自调用会导致切面失效。比如一个Service里有两个方法方法A内部直接调用了同类的方法B而方法B上有Transactional或自定义日志注解调用方只通过接口调了方法A那方法B上的事务注解不会生效。原因是AOP代理只对方法A生效方法A内部的this.methodB()是调用原始对象不是代理对象。解法是在方法A里注入自身代理对象或者把方法B拆到另一个Service里。4.2 Spring Boot自动配置与启动流程自动配置是一个高频考点。SpringBootApplication是SpringBootConfiguration、EnableAutoConfiguration、ComponentScan三个注解的组合。EnableAutoConfiguration会通过AutoConfigurationImportSelector加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中的配置类再配合ConditionalOnClass、ConditionalOnMissingBean等条件注解决定是否生效。原卷里有一道题是问“为什么Spring Boot能自动帮我们配好数据源”。我当时答的是如果classpath下存在DataSource类且没有自定义DataSourceBean自动配置会创建HikariCP连接池并读取spring.datasource.*配置。所以想让某个自动配置失效可以加一个自定义Bean覆盖也可以用exclude属性排除。4.3 微服务拆分与服务治理贝壳的业务体系里房源、楼栋、户型、经纪人、带看、合同、支付这些模块天然适合微服务化。卷子里有一道问服务拆分的题不是在问用什么框架而是问“你会按什么原则拆”。我的回答是按业务域拆、按变更频率拆、按团队归属拆同时避免过度拆分一个3人小团队维护十几个微服务只会带来灾难。服务治理相关考点包括注册中心选型、配置中心、负载均衡、熔断降级。注册中心我重点准备了Nacos和Eureka的区别Nacos支持AP和CP两种模式切换Eureka只保证APNacos同时具备配置中心能力生态更贴近Spring Cloud Alibaba。熔断这块Sentinel和Hystrix的对比也常考核心是计数窗口、滑动窗口、信号量隔离这些底层机制。5. 数据库、缓存与分布式高并发读写场景下的必答题5.1 MySQL索引是必考不能只会背B树数据库题在这套卷子里的分量很高。第一梯队就是索引问法通常是为什么不选哈希索引、为什么不选二叉树而要选B树B树的优势要能从磁盘IO角度讲树高矮三层B树能存千万级数据叶子节点用双向链表串联非常适合范围查询和排序节点大小和操作系统的页对齐一次IO能加载更多索引项。哈希索引只适合等值查询范围查询直接失效。然后是最左前缀原则。我复盘时专门找了一道联合索引题(city, district, community)这个索引哪些查询能走索引city、city district、city district community能走district community走不了因为跳过了最左列。还要注意范围查询会让右边的列失效比如city 北京 AND district 朝阳 AND community 望京SOHOcommunity就不会走索引。回表和覆盖索引也是一对高频组合非聚簇索引查到主键再用主键回表查完整行这就是回表如果索引本身就包含了查询需要的所有字段就是覆盖索引可以避免回表性能翻倍。实际优化时可以用explain看type和Extra列出现了Using filesort或Using temporary基本就要动手优化SQL了。5.2 Redis缓存穿透、击穿、雪崩这三座大山是缓存题的常青树。穿透是查一个必然不存在的数据请求直接打到数据库解法有三种缓存空对象设置短过期时间、布隆过滤器前置拦截、参数合法性校验。房产搜索里用户输入一个不存在的房源ID布隆过滤器就能在缓存和数据库之前把它拦掉。击穿是某个热点key瞬间失效大量请求同时打到数据库。解法热点key不设置过期时间或设置长过期时间、用互斥锁让只有一个请求去重建缓存、逻辑过期方案。雪崩是大量key在同一时间集体失效或者Redis节点宕机。解法过期时间加随机值、多级缓存、Redis高可用集群。缓存与数据库的一致性也考了一个经典场景先更新数据库还是先删缓存我给出的结论是“先更新数据库再删除缓存”配合延迟双删或者订阅binlog异步删除兜底。这里要注意强烈不建议先删缓存再更新数据库因为删缓存后一有读请求打到数据库读旧数据又把手里的旧值写回缓存缓存就永远是脏的了。5.3 分布式事务与消息队列房产交易里下单、锁房、创建合同是一个典型分布式事务场景因为订单服务、房源服务、合同服务分属不同库。2PC的性能太差不适合在线交易TCC的落地成本高比较常用的是可靠消息最终一致性方案。消息队列还有一个核心考点是幂等消费者收到重复消息时不能重复扣款、重复锁房。解法是消费端做幂等表用消息唯一ID去重或者利用业务主键唯一约束来防重。顺序消息也是高频难点比如一个房源的上下架消息必须严格有序否则先上架后下架的两次消息如果乱序房源状态就错了。RocketMQ支持队列级别的顺序消息Kafka则要靠单分区加按业务ID做key来保证分区内有序。做完这套卷子后我把这几个知识点在项目里还原了一遍发现分布式事务的核心不是选一个中间件而是要想清楚每个环节失败后的补偿路径。这部分内容在正常简历项目描述里很难写清楚但笔试时能答完整就是加分项。5.4 分布式锁的实现与坑分布式锁考的是多实例下互斥。Redis方案用SET key value NX EX seconds保证原子性正确的解锁用Lua脚本保证“判断是不是自己的锁”和“删除锁”两步原子执行。Redisson的看门狗机制可以自动续期避免业务执行时间超过锁过期时间导致提前释放。这套卷子没有直接考代码实现但我复盘的时候把它当成了必会代码因为跟后面的设计题强相关。选型的结论是小规模场景用Redis锁就够了如果对强一致性要求很高用ZooKeeper临时顺序节点因为ZooKeeper在节点删除时会主动通知客户端释放锁避免了Redis锁过期时间不好控制的缺陷。// 用RedisTemplate手工实现分布式锁的加锁与解锁 String lockKey stock:1001:lock; String requestId UUID.randomUUID().toString(); Boolean locked stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 业务逻辑 } finally { String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), List.of(lockKey), requestId); } }6. 系统设计题从“小区房源浏览”到“高峰秒杀”的答题思路6.1 一道典型设计题的完整推演这套卷子的压轴题是一个贴合贝壳业务的场景某热门新盘开盘放出100套房源大量用户同时在线抢看房资格请设计这个系统。不夸张地说这个题的答案几乎能看出一个人是“接口仔”还是“架构师”。我当时的回答分几步走。第一步先澄清需求边界房就100套目标用户量是十万级还是百万级抢到资格后需要在多长时间内确认这些问题如果不问清楚就直接开方案基本会被判低分。第二步是容量估算假设10万用户同时在线峰值QPS按总用户量的20%计算压到2万QPS这就是一个中高并发系统需要缓存、异步、限流三者配合。第三步是整体架构网关层做全局限流和用户身份校验Nginx、Sentinel都行应用层用Redis预扣库存防止超卖数据库层只保存最终结果确认资格时引入MQ削峰让用户提交的请求先进入队列再由消费者平滑地写库。第四步是防重与幂等用户重复提交时前端置灰后端用请求唯一ID做幂等Redis里以用户ID为维度加分布式锁防止同一个人把多个看房资格都锁走。最后一步是容错和扩展Redis宕机怎么办本地缓存兜底加限流MQ积压怎么办监控告警加消费者扩容。整个答案是有完整链路和取舍依据的而不是堆一堆中间件名词。6.2 设计题的评分标准和表达技巧复盘过几套大厂设计题之后我发现评分维度基本是五个需求澄清、容量估算、架构合理度、核心流程完整性、扩展性与容错性。表达顺序很重要建议按“需求 - 估算 - 架构 - 核心流程 - 容错”来讲不要一上来就画一张包含十几个组件的架构图。设计题的高分关键不是用了多高级的中间件而是每一步都能解释“为什么”。比如为什么用Redis而不是数据库直接扛因为2万QPS对MySQL来说已经非常危险而Redis单实例就能扛十万级QPS。为什么用MQ因为用户确认资格这个动作不需要立刻完成可以异步化既能削峰又能解耦。这些理由比名词本身值钱得多。7. 复盘总结刷题之外这些经验和认知才是最重要的7.1 我在复盘过程中发现的常见盲区整套卷子刷完后我总结了几个反复出现的盲区也是绝大多数Java求职者最容易翻车的地方。第一个盲区是“会背概念但不会解释取舍”。比如能背出B树的所有优点但说不出为什么不选跳表能背出ConcurrentHashMap在JDK 8用了CAS加synchronized但说不清为什么锁粒度更细了还要用synchronized而不是全CAS。面试官想听的不是定义而是你脑子里有没有一张完整的知识地图。第二个盲区是“不懂业务场景”。同样的知识点放在纯内存计算里和放在房产交易链路里考点完全不同。贝壳这套卷子大量题目隐含了业务背景说明他们更看重候选人把技术应用到具体场景的能力。我建议准备笔试时多做一步每个核心题目反推一个业务场景再用技术方案去解决它。第三个盲区是“复习没有主线”。Java基础、集合、并发、JVM、Spring、MySQL、Redis、分布式、设计题这九个模块是有依赖关系的。我建议按“语言基础 - 集合 - 并发 - JVM - Spring - MySQL - Redis - 分布式 - 设计题”的顺序复习前面不扎实后面一定会卡壳。7.2 给春招人的备考建议如果距离笔试还有两到三周我建议把时间切成三块。第一块是快速过核心八股文把高频考点整理成自己的知识图谱重点标注那些“能展开三层追问”的知识点第二块是刷真题每套题做完不要急着做下一套把错题对应的知识盲区重新补一遍形成错题本第三块是动手写代码笔试里的编程题不能只讲思路一定要落实到IDE里跑一遍尤其是线程池、分布式锁、并发扣库存这类代码光看是不行的。有一件事我不太推荐疯狂背题。贝壳这种业务型公司的笔试题目本身重复出现的概率不高但核心考点高度一致。你把HashMap的源码读透、能把B树的每一层为什么说清楚、能徒手写一个Redis分布式锁比背一百道“HashMap和Hashtable区别”有用得多。7.3 最后分享一个笔试实战的小技巧最后说一个我在整理这套卷子时特别有体会的细节。笔试的时候时间非常紧张选填题不要恋战一道概念题如果超过两分钟还犹豫不决先标记跳过把时间留给后面的编程题和设计题。因为设计题的分数权重极高而且能展示整体架构能力前面丢几分完全可以在后面找回来。还有一个很多人会忽略的点编程题哪怕不是最优解也一定要把核心思路和算法注释写在代码上方。阅卷人能看到你的思路只要你方向对即使实现有小Bug也会给不少步骤分。我朋友当时就是因为一道并发题想到了CAS但没写完只写了注释最终依然过了笔试。别让“差一点写完”变成“连思路都没留”。