
每年三四月份都是Java工程师跳槽和校招的集中期贝壳找房作为房产交易平台里技术投入比较重的一家它的春招笔试卷在业内一直有不错的参考价值。我最近正好完整刷了一遍贝壳找房2023年春招Java工程师笔试卷2整体感受是题目不偏、不怪但覆盖面很广基础题占了大头同时穿插了几道需要真正理解原理才能答对的“拦路虎”。这份试卷对正在准备Java实习或校招的同学来说是一份很值得用来查漏补缺的实战材料。这份卷子整体分为四大部分单选题、多选题、SQL题、编程题。 从考点分布来看Java基础语法、集合框架、JVM内存模型、并发编程、Spring框架、MySQL索引与事务、Redis缓存、分布式一致性这些是绝对的高频区。尤其是Java并发和JVM相关题目几乎每场大厂笔试都会出现贝壳也不例外。下面我就结合这份试卷的题目把每一类的核心考点、解题思路和容易踩的坑完整拆一遍。1. 试卷整体风格与考点布局先别急着做题把整张卷子的考察逻辑摸清楚比盲目刷题有用得多。贝壳这套笔试卷的命题风格可以总结为三句话基础题考深度、框架题考场景、编程题考边界。1.1 题型配比与分值分布从题型结构来看50道选择题含单选和多选占了大概60%的分值剩下40%集中在两道SQL题和两道编程题上。选择题里Java基础集合、String、异常、泛型大约占15题JVM和并发各自占8-10题Spring和MySQL各占5题左右Redis和分布式加起来占剩下的部分。这个配比其实反映了大厂对Java工程师的核心预期基础知识必须扎实同时对真实生产环境里常用的组件MySQL、Redis、Spring要有场景化理解而不是停留在背概念。很多人会忽略多选题。贝壳这套卷子的多选题共10道左右每道题的选项数量从4到6个不等少选得一半分、多选或错选不得分。这种评分规则意味着你不仅要能判断“哪个选项对”还要能判断“哪个选项不对”对知识的准确度要求比单选题高一个档次。我们平时复习的时候很容易只记结论不记限定条件比如“HashMap是线程不安全的”这句话本身没错但放到多选里如果选项说“HashMap在单线程下是线程安全的”很多人就会犹豫。实际上HashMap在单线程下不存在竞争自然会被认为是线程安全的但严格来说它并没有做任何线程安全保证——这种细微差别就是多选题的命题空间。1.2 高频考点明细表根据我对这套试卷以及近年来贝壳、链家系其他技术岗笔试题的对比分析考点分布基本落在下表这些方向考察模块核心知识点出题形式出现频率Java基础String、包装类缓存、异常体系、泛型擦除单选极高集合框架HashMap底层、ConcurrentHashMap分段、ArrayList扩容单选/多选极高JVM内存区域划分、GC算法、类加载双亲委派单选/多选高并发编程synchronized与ReentrantLock、volatile语义、AQS原理单选/多选高SpringBean生命周期、事务传播机制、循环依赖单选中高MySQL索引失效场景、事务隔离级别、MVCC单选/SQL题高Redis缓存穿透与击穿、持久化、分布式锁单选/编程中高分布式幂等性、接口设计、消息队列编程/设计中这里有一个值得注意的点选择题目里几乎没有出现“手写单例模式”“判断输出结果”这类烂大街的题而是更多换成了一种变体——给一段代码问这段代码在什么情况下会出问题或者这段代码能承受多大的并发量。这是贝壳系笔试的一个特色它考察的不是你会不会写一个知识点而是你知不知道这个知识点在生产环境里为什么重要。2. 核心考题深度解析Java基础与集合框架这一部分是整张卷子的地基也是很多自认为“基础不错”的同学真正翻车的地方。我把其中最有代表性的几类题目拿出来逐个拆解背后的逻辑。2.1 String与包装类的“送命题”变形试卷里有一道这样的题String s1 abc; String s2 new String(abc); String s3 s2.intern(); System.out.println(s1 s2); System.out.println(s1 s3);这题考察的是字符串常量池与intern方法的语义。我估计大部分人能答对第一个输出false第二个输出true。但贝壳随后追问了一个变体Integer a 127; Integer b 127; Integer c 128; Integer d 128; System.out.println(a b); System.out.println(c d);这道变体的价值在于Integer的valueOf方法在-128到127之间有缓存所以a b是true而c d是false。很多人在这个标准答案上没问题但再变一下就会露馅Integer e new Integer(127); Integer f new Integer(127); System.out.println(e f); System.out.println(e a);无论值是否在缓存范围内只要使用了new关键字就会在堆上创建新对象所以e f是falsee a也不能为true。这套追问的意义在于大厂已经不再满足于考察你是否知道Integer缓存而是考察你是否真正理解对象引用和缓存机制之间的关系。换成Long、Short、Byte也是同样的规律但Float和Double没有缓存实现因为浮点数的相等性判断本身就没有广泛使用的必要——这一点在选择题选项里出现过属于较冷门的知识点。再往深一层String的intern()方法在JDK 7以后把字符串常量池移动到了堆中这样做的好处是常量池不再是固定大小可以随着堆扩容坏处是intern()方法如果被滥用会加重堆内存的占用和GC的负担。如果面试官顺着这题继续追问“什么场景下不建议使用intern”你要能说出“在大量动态生成相似字符串的系统中intern会造成堆中重复字符串的持久化引用阻碍垃圾回收”这个层面的理解。这些都是刷题背结论无法覆盖的深度却是笔试卷上拉开差距的关键。2.2 HashMap的底层原理与并发问题贝壳在选择题中直接考察了HashMap的底层数据结构变化历程。JDK 1.7中HashMap采用数组链表的结构JDK 1.8改为数组链表红黑树当链表长度超过8且数组容量大于等于64时链表会树化为红黑树。这个“8”和“64”两个阈值几乎年年考但真正容易被忽略的是为什么是8而不是7或9。这里有一个统计学背景在随机哈希码的情况下链表节点数达到8的概率约为千万分之六这个概率已经足够低说明大多数时候链表长度不会超过8。但如果出现了超过8的情况说明要么哈希函数设计有问题导致冲突严重要么是有人恶意构造哈希碰撞企图发起Hash攻击。红黑树的结构能在O(log n)的复杂度下完成查找把最坏情况从O(n)优化到O(log n)从安全角度讲也具备一定防御意义。不过要注意如果数组容量小于64即使链表超过8也不会树化而是优先扩容数组因为扩容能让元素重新散列、降低冲突比直接树化更高效。这题的延伸是“put操作在什么时候会触发扩容”。很多人的答案是“元素个数超过负载因子乘以数组容量时”这个没错但不够完整。真正的扩容触发点有两个一是size threshold其中threshold capacity * loadFactor二是在树化之前如果tab.length MIN_TREEIFY_CAPACITY(64)也会先扩容而不是树化。这两个触发条件缺一不可。另外并行环境下HashMap会出现的典型问题包括JDK 1.7中的头插法在多线程扩容时可能形成环形链表导致get操作死循环JDK 1.8中虽然改成了尾插法避免了环形链表问题但数据丢失、size不准确的问题依然存在。所以HashMap从头到尾都不适合在多线程环境下使用要使用ConcurrentHashMap而不是用Collections.synchronizedMap包一层应付了事。2.3 ArrayList扩容机制与AbstractList的快速失败还有一道题考察的是ArrayList扩容后的新容量。默认情况下ArrayList第一次添加元素时容量从0扩容到10之后每次扩容为原来的1.5倍也就是newCapacity oldCapacity (oldCapacity 1)。如果用户通过构造函数指定了初始容量比如new ArrayList(15)那么第一次添加元素时不会触发扩容而是等到第16个元素添加时才触发新容量变成2215的1.5倍。这个考点本身不难但“快速失败机制”这个概念让不少人在判断题上栽了跟头。ArrayList的Iterator在迭代过程中如果检测到modCount发生变化会立即抛出ConcurrentModificationException。这里需要注意一个细节for-each循环的本质就是iterator所以以下代码必然抛异常ListString list new ArrayList(); list.add(a); list.add(b); for (String s : list) { if (s.equals(a)) { list.remove(s); } }很多人会想当然认为“只删一个元素不会出问题”但modCount从2变成3迭代器维护的期望值还是2检测到不一致就抛异常。正确的删除方式是使用Iterator.remove()方法因为该方法会同步更新期望的modCount。这一题在试卷里是以判断改错形式出现的考察的正是基础代码的边界意识。我在实际生产中确实遇到过线上服务因为类似代码导致批量任务失败的case后来整个团队都约定遍历时删除一律用迭代器。这已经不是笔试考点的问题而是工程规范问题。3. JVM与并发编程笔试中的“分水岭”JVM和并发是Java笔试里最能拉开区分度的两个板块也是贝壳这套卷子的压舱石。没有背熟这两块选择题正确率很难超过80%。3.1 JVM运行时内存区域与GC回收的判定逻辑试卷中有道简答式选择题给了四个内存区域堆、虚拟机栈、本地方法栈、程序计数器问“哪个区域在特定条件下不会抛出OutOfMemoryError”。答案是程序计数器。因为程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。这个冷门知识点考得相当细节如果能答对说明基础是真的扎实。更经典的还有Java堆的划分。JDK 8以后方法区被移除了永久代的概念改成了元空间Metaspace使用本地内存来存放类的元数据。这里有一个常见误区很多人把“字符串常量池在JDK 7之后移到了堆中”和“运行时常量池在元空间中”混淆。实际上运行时常量池依然属于方法区的一部分JDK 8里就位于Metaspace而字符串常量池在JDK 7开始就已经移到了堆中两者位置不同、存储内容不同、回收机制也不同。选择题里专门有一道题考察了这个区别四个选项分别是“字符串常量池在元空间”“运行时常量池在堆中”“字符串常量池在堆中”“运行时常量池在方法区中”标准答案是后两者。GC方面判断对象是否存活的标准是GC Roots是否可达。所谓GC Roots包括虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中的静态属性引用对象和常量引用对象、被synchronized加锁的对象等。不可达并不等于立即被回收还需要经过两次标记过程第一次标记后判断是否有必要执行finalize()如果对象覆盖了finalize()且未被激活会进入F-Queue队列等待虚拟机自动调用。这一块在试卷里出成了多选题哪些对象可以作为GC Roots正确选项是“虚拟机栈中局部变量表引用的对象”和“方法区中类静态属性引用的对象”而“被final修饰的常量”需要看它是否指向对象且存放在常量池中属于常量引用也可以算GC Root——这个选项有很多人漏选。关于GC算法新生代使用复制算法老年代使用标记-清除或标记-整理。为什么新生代不能直接用标记-清除因为标记-清除会产生内存碎片而新生代的GC频率高、存活对象比例低复制算法的代价相对可控。为什么老年代不能用复制算法因为老年代对象存活率高复制算法需要额外空间来存放存活对象成本太高。所以JVM设计者根据对象生命周期特征选择了不同的回收策略这不是拍脑袋决定的背后是空间利用率和GC效率之间的权衡。3.2 synchronized与volatile的现代视角并发编程题里有一道非常典型的class Counter { private int count 0; public synchronized void increment() { count; } }问这个increment方法能保证线程安全吗如果count用volatile修饰后能保证线程安全吗第一个问题答案是“能”。因为synchronized在进入和退出同步代码块时会分别执行加锁和解锁操作锁的获取会强制刷新工作内存中的变量值锁的释放会把修改同步回主内存所以count这个“读-改-写”操作在同步块内是原子性、可见性都得到保证的。第二个问题答案是“不能”。volatile只能保证可见性和有序性但不能保证原子性而count是复合操作即使都看到了最新值依然存在多个线程“同时读到同一个旧值各自加1后再写回”的场景所以最终结果会小于预期值。这个基础题不会有人答错但以下几道变体可能才是真正的陷阱volatile boolean flag false; // 线程A flag true; // 线程B while (!flag) { }这道题考察volatile在“状态标志”场景下的正确用法。因为对flag的写入是单一赋值操作不依赖当前值所以不存在原子性问题volatile的可见性和有序性能够保证线程B一定能看到线程A的修改。volatile也不能完全禁止重排序它通过内存屏障禁止的是特定指令的重排序而不是所有重排序。JMM中关于volatile的重排序规则表是如果第一个操作是volatile读则不管第二个操作是什么都不能重排序如果第二个操作是volatile写则不管第一个操作是什么都不能重排序如果第一个操作是volatile写、第二个操作是volatile读则不能重排序。这个规则表在多选题中出现过解释起来就是volatile读之后不能有普通变量写排到它前面volatile写之前不能有普通变量读写排到它后面。更深入的并发机制是AQSAbstractQueuedSynchronizer。ReentrantLock、CountDownLatch、Semaphore和ReentrantReadWriteLock都基于AQS实现。AQS的核心是一个volatile int state状态字段和一个CLH变体队列。以ReentrantLock为例state表示锁的重入次数当前线程获取锁时如果state 0则CAS尝试置为1表示获得锁如果当前线程已经持锁则state加1释放时依次减1直到0。CLH队列中的等待节点通过前驱节点的waitStatus来判断是否需要阻塞。这一套机制看起来抽象但笔试里真正考的是“公平锁与非公平锁的差异”非公平锁在获取锁时会先尝试一次插队CAS如果失败再排队公平锁则严格按排队顺序获取。所以非公平锁在竞争激烈时可能导致某些线程长时间饥饿但它的吞吐量通常更高因为它减少了线程唤醒造成的上下文切换。3.3 线程池参数设计与拒绝策略线程池是并发题里性价比最高的一道必考题这在贝壳试卷里同样没有缺席。题目给出了一个典型场景一个系统每秒接收500个请求每个请求处理耗时200ms要求不堆积请求问如何设置核心线程数。计算公式是并发线程数 每秒请求数 × 平均响应时间。500 × 0.2 100个并发线程数。但这只是理论值在实际生产环境中还需要考虑CPU核数、IO等待占比和任务队列的长度。如果是CPU密集型任务线程数设为CPU核数 1比较合适如果是IO密集型任务线程数可以设为CPU核数 × (1 平均等待时间 / 平均计算时间)。比如一个服务部署在4核机器上IO等待时间占比80%计算时间占比20%那么线程数理论上可以设置为4 × (1 0.8 / 0.2) 20。拒绝策略的选择也是一道多选题的考点。AbortPolicy是默认策略直接抛RejectedExecutionExceptionCallerRunsPolicy让提交任务的线程自己执行该任务相当于是把压力倒推给调用方这是一种天然的背压机制DiscardOldestPolicy丢弃队列中最旧的未处理任务适合允许丢弃任务的应用DiscardPolicy直接丢弃新任务比较粗暴但不会抛异常。实际工作中对于不可丢失的任务我通常选择CallerRunsPolicy因为它的行为最可控——任务不会丢只是执行速度会受限于调用方线程的执行能力。而AbortPolicy容易被忽略一旦触发就直接抛异常如果没有配套的监控报警很容易造成业务静默失败。4. MySQL与SQL题不只是写出来还要写对贝壳这套笔试卷的SQL题不算难但非常典型。一道是“查询每个部门工资最高的员工”另一道是“统计各状态订单数并排序”。如果你只会写GROUP BY而不会窗口函数第二题也许能勉强通过但要拿到满分还有点悬。4.1 经典“每组TopN”查询的三种写法题目给出了员工表CREATE TABLE employee ( id INT PRIMARY KEY, name VARCHAR(50), department VARCHAR(50), salary DECIMAL(10, 2) ); -- 查询每个部门工资最高的员工信息我推荐的写法是使用窗口函数简洁且通用SELECT department, name, salary FROM ( SELECT department, name, salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rk FROM employee ) t WHERE rk 1;这里用RANK()而不是ROW_NUMBER()是因为存在同分情况时工资相同的人应该同时被列出。如果业务方只想取一个人那应该用ROW_NUMBER()同时要明确告诉业务方“相同工资取谁是不确定的”——这个决定不能由SQL隐含完成需要显式加二级排序条件。如果不支持窗口函数比如面试官限定MySQL 5.7可以用关联子查询SELECT e.department, e.name, e.salary FROM employee e WHERE e.salary ( SELECT MAX(salary) FROM employee WHERE department e.department );这种写法对索引要求比较高大表情况下性能堪忧但笔试试卷里把逻辑写对即可。我自己更推崇的第三种方式是用LEFT JOIN但它在理解上不如前两种直观这里不展开。4.2 索引失效场景与优化方向SQL题之外试卷的选择题部分还有不少MySQL索引的题这里几乎必考索引失效的场景列表对索引列使用函数或表达式计算WHERE YEAR(create_time) 2023会导致索引失效隐式类型转换WHERE phone 13800138000如果phone字段是varchar类型会发生隐式转换左模糊查询LIKE %abc索引失效联合索引不满足最左前缀原则(a, b, c)联合索引查询条件只有b时索引失效OR连接非索引列WHERE a 1 OR b 2如果b不是索引列整个查询无法走索引在这一题之上贝壳加了一个追问如果SQL里的OR连接的是同一个索引列WHERE a 1 OR a 2会走索引吗答案是会的因为MySQL可以将它优化为a IN (1, 2)的形式进而走索引。但如果两个条件列不同优化器就很难处理了。这个细节一般人不注意但在多选题里作为选项出现时非常容易被误判。事务隔离级别和MVCC也是必考项。MySQL默认的REPEATABLE READ隔离级别下可以防止脏读和不可重复读但不能完全防止幻读。InnoDB通过间隙锁Gap Lock和next-key lock在特定场景下解决了幻读问题但前提是查询条件能利用索引。MVCC的核心是三个隐藏列DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID以及ReadView机制。REPEATABLE READ下ReadView只在第一次读取时创建后续复用READ COMMITTED下每次读取都会生成新的ReadView。这就是为什么REPEATABLE READ能保证可重复读而READ COMMITTED不能的关键。理解这一层逻辑比死记硬背隔离级别表格有用得多因为在多选项里命题人往往会把一个错误的结论伪装成正确表述比如“REPEATABLE READ下所有事务都不会出现幻读”——严格来说这句话不正确因为它忽略了当前读加锁读的情况。5. 编程题与场景设计从“能跑”到“能上线”贝壳这套卷子的编程题有两道一道是“实现LRU缓存”另一道是“设计一个线程安全的计数器”。这两道题都不难但想拿满分不容易因为它们对代码的健壮性和并发安全有隐含要求。5.1 LRU缓存的高效实现LinkedHashMapLRU最近最少使用缓存是面试里的常客。最直接的写法是继承LinkedHashMap重写removeEldestEntry方法import java.util.LinkedHashMap; import java.util.Map; public class LRUCacheK, V extends LinkedHashMapK, V { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); this.capacity capacity; } Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() capacity; } }代码本身非常精简但理解accessOrder参数是关键。构造函数第三个参数传true表示按访问顺序排序每次get或put都会把对应的Entry移动到链表尾部所以链表头部就是最久未被访问的元素。默认传false表示按插入顺序排序那个不是LRU语义。如果要求手写实现而不使用LinkedHashMap那就需要自己维护一个双向链表加HashMap的组合。注意这里的实现细节链表节点需要同时保存key和value因为在链表超过容量需要淘汰时我们要通过节点的key去HashMap中删除对应条目。如果节点只存value淘汰时就要遍历HashMap才能找到key复杂度变成O(n)不合格。这一处是考察“数据结构设计的闭环性”的关键点也是阅卷时区分高分答案和普通答案的地方。5.2 线程安全计数器的三种方法第二道编程题要求实现一个线程安全的计数器并提供increment()和getCount()两个方法。基础版本用synchronized关键字public class SafeCounter { private long count 0; public synchronized void increment() { count; } public synchronized long getCount() { return count; } }这个写法没问题但只能算及格。更优的解法是使用AtomicLongimport java.util.concurrent.atomic.AtomicLong; public class AtomicCounter { private final AtomicLong count new AtomicLong(0); public void increment() { count.incrementAndGet(); } public long getCount() { return count.get(); } }AtomicLong通过CAS比较并交换实现线程安全在低竞争场景下性能比synchronized好但在高竞争场景下CAS会频繁自旋导致CPU占用高。JDK 8中引入了LongAdder它通过分段累加的思路在高并发场景下进一步提升了性能。把三种方案的特点写清楚同时结合并发量给出选型建议才说明你真懂并发编程。此外要指出一点如果increment被大量调用而getCount几乎不调用LongAdder是首选如果读写都很频繁就需要做压测来确认选型不能拍脑袋。5.3 分布式场景设计秒杀系统防超卖编程题之外卷子最后还有一道开放式设计题要求设计一个商品秒杀系统防止商品超卖。这类题目在笔试中出现通常不要求完整代码但需要写出核心思路和关键命令。我给出的方案包含三层防线应用层用户请求先进入Redis使用DECR或Lua脚本扣减库存库存不足直接返回“已售罄”数据库层UPDATE stock SET stock stock - 1 WHERE id ? AND stock 0利用行锁保证最终一致性幂等层同一用户同一商品只能下单一次通过唯一订单号或用户ID加商品ID的唯一索引来约束Redis扣减库存的Lua脚本核心如下if redis.call(GET, KEYS[1]) false then return 0 end local stock tonumber(redis.call(GET, KEYS[1])) if stock 0 then redis.call(DECRBY, KEYS[1], ARGV[1]) return 1 end return 0为什么用Lua脚本因为Redis保证Lua脚本内的多条命令在单线程中执行不会被其他客户端命令插入本质上是原子操作。如果不使用Lua脚本而是先GET再DECR在并发场景下两个请求可能同时GET到库存剩余1然后各自DECR最终库存变成-1就是超卖。脚本把“判断库存”和“扣减库存”合并为一个原子操作从架构上杜绝了这个问题。数据库层的UPDATE ... WHERE stock 0也很关键它利用行锁的互斥特性来保证即使Redis被绕过或者Redis数据丢失数据库最终也不会出现负库存。这个设计里还有一个细节更新结果影响行数为0时说明库存不足需要回滚事务并返回失败不能简单地以“执行SQL未报错”来判定成功。6. 常见问题与备考策略最后总结一下这套卷子暴露出来的几类典型问题和对应的备考策略这部分比单纯对答案更有参考价值。6.1 概念混淆型错误贝壳这套卷子命题人非常喜欢在概念边界上做文章。典型例子包括把“字符串常量池在JDK 7后移入堆”与“运行时常量池在元空间”搞混把volatile能保证可见性和有序性误认为能保证原子性把ConcurrentHashMap的弱一致性误认为强一致性——它的size()方法和isEmpty()方法返回的都是近似值多个线程并发写入时不能保证获取到准确的实时大小把ThreadLocal的内存泄漏原因记错——ThreadLocal中Entry的key是弱引用、value是强引用当ThreadLocal对象被外部释放后key会被回收但value仍然被堆中的Entry强引用如果不调用remove()value就永远无法被回收。这个考点在很多八股文里讲不透但大厂就是喜欢考针对这类问题我建议建立一张“易混淆知识点对照表”把成对出现的概念放在一起横向对比而不是单独记忆。每次刷完题把自己做错的概念写进表格里考前重点复习就可以了。6.2 手写代码不规范LRU缓存和线程安全计数器这种题代码本身不复杂但很多人写得不够完善没有考虑容量为0或负数时的异常处理没有指定泛型没有考虑到removeEldestEntry的参数类型应该是Map.EntryK, V而不是裸的Entry。阅卷系统一般不会执行代码而是人工评审代码的风格规范和边界处理直接决定评分档位。我建议平时刷题时养成三个习惯第一所有自定义类都加上必要的构造校验第二涉及集合框架的代码显式声明泛型第三多线程代码必须明确指出哪些方法是线程安全的哪些不是。这三个习惯在笔试中会让你的答案明显高于平均水平。6.3 做题节奏分配不合理50道选择题加2道SQL题加2道编程题90分钟的考试时间非常紧张。计算下来选择题平均每题只有1分钟多一点的时间编程题至少要留30分钟。如果一道选择题卡了3分钟以上应该立刻标记跳过不要恋战。这一条说着容易做着难但高分选手的共同特征就是基于确定性来分配时间而不是被单题难度裹挟。6.4 一些值得长期坚持的备考方法关于备考资料的选取Java八股文是起点但远远不够。笔试题目越来越偏向“原理场景”的组合考察单纯背结论已经很难拿到高分。我的建议是每复习一个知识点就用“是什么—为什么—什么时候用—什么时候不能用”四段式结构来整理笔记然后配套找1-2道对应的真题进行验证。这样做的效果比盲目刷300道题要好得多。此外强烈建议把笔试中写过的代码保存下来面试时直接作为项目中的技术选型参考。比如LRU缓存的实现我在实际项目中就真的用在了用户会话管理上线程安全计数器的LongAdder方案我也用在了某个高并发埋点服务的统计逻辑中。笔试和工程实践之间的距离没有大家想象中那么远。你对试卷上每道题的思考深度最终会通过代码质量和系统稳定性体现出来。