这两年秋招笔试我是真没少刷,各大厂的卷子基本都见过一遍。途虎养车这套2023秋招Java笔试试卷B,在牛客和脉脉上被讨论的频次不低。它不算那种硬核到劝退的题库,但胜在覆盖面完整:Java基础、JVM、并发、Spring生态、MySQL、Redis、算法编程,甚至还有结合业务场景的设计题,几乎把互联网公司后端Java岗的核心考点全部囊括进去了。
不管你是准备投途虎,还是想拿这套卷子当模拟题练手,我都建议认真过一遍。这篇文章不打算逐题贴答案,而是把试卷B背后真正想要考察的能力模型拆给你看,顺便把答题时容易踩的坑、准备时容易被忽视的点都梳理出来,希望能帮你少走弯路。
1. 试卷B的定位:途虎秋招笔试到底想筛什么样的人
1.1 途虎的业务形态决定了考点方向
先聊一个很多人忽略的问题:为什么这套卷子的考点长这样?
途虎养车本质上是汽车后市场的在线服务平台,线上有商城、预约、会员系统,线下有大量直营和加盟门店,背后还连着一整套供应链、库存、订单、支付、物流体系。这种业务形态决定了它招的Java工程师,不是去写底层框架或中间件的,而是做业务系统的——也就是大量订单流、库存流、会员流的高并发读写、数据一致性、缓存设计、消息异步处理。
所以你看试卷B,它不会考你怎么实现一个JVM、怎么从零写一个RPC框架,而是重点考察这些内容:
- Java基础是否扎实,能不能写出健壮的代码;
- 有没有处理过并发、内存这类真实生产问题;
- 对Spring Boot这类主流框架的原理理解到不到位;
- 会不会用MySQL索引、事务、Redis缓存解决实际业务问题;
- 能不能在白板/在线编辑器里快速写出一道中等难度的算法题。
说得直白点,这套卷子筛选的是"能干活、能扛事、基础没硬伤"的后台业务开发,而不是纯粹的算法竞赛选手或者框架调包侠。
1.2 题量、题型与分值结构参考
据我拿到试卷B的同学反馈和自己的刷题印象,这套卷子的题型大致是:
| 题型 | 大致占比 | 考察内容 |
|---|---|---|
| 单选题/多选题 | 25%-35% | Java语法、集合、异常、面向对象概念 |
| 判断题 | 5%-10% | 容易混淆的细节知识点 |
| 简答题 | 15%-20% | JVM内存、垃圾回收、线程池、数据库事务等 |
| SQL/数据库题 | 10%-15% | 索引设计、SQL编写、事务隔离级别 |
| 编程题 | 20%左右 | 排序、链表、字符串处理、中等难度LeetCode |
| 场景设计题 | 10%左右 | 订单超时、库存扣减、缓存雪崩等业务问题 |
这个结构有一个很现实的意义:它决定你答不完也能过。笔试不是要求你拿满分,而是考察你在有限时间里的取舍能力。选择题、判断题那种一眼能出的题,尽量快速拿下;简答题答到点子上、条理清楚;编程题至少完整AC一道;场景题把思路写清楚,哪怕没写完整代码也有分。
顺便说一句,过了笔试之后,这个成绩会同步到后续面试官那边的评估表里。笔试里暴露出来的薄弱点,非常大概率会在一面二面中被追问。所以笔试不只是为了"过",更是为了摸清自己的短板再针对性准备。
提示:试卷B既然是B卷,说明还有A卷甚至C卷。不同考场的题目并不相同,但考点覆盖会被有意对齐。所以刷B卷的价值不是背原题,而是覆盖考点。
2. Java基础题:集合、异常、语言特性的踩分点
2.1 HashMap和ConcurrentHashMap,必考中的必考
在试卷B的选择题和简答题里,HashMap相关的题目基本没有缺席过。它几乎是最能区分"背过八股"和"真正理解"的知识点。
第一个高频考点:HashMap的数据结构。JDK 8之后是数组加链表加红黑树。当链表的长度超过8,并且数组长度达到64时,链表会转成红黑树,目的是把最坏情况下的查找复杂度从O(n)降到O(logn)。但树化不是常态,节点数减少到6以下时又会退化成链表,避免红黑树的自旋在数据量小时反而浪费性能。
第二个高频考点:put操作的完整流程。计算hash时先对key的hashCode做一次高16位与低16位的异或扰动,目的是让高位也参与到数组下标的计算中,减少冲突。然后用(n-1)&hash计算桶下标,JDK 8的公式不用取模运算,而是用位运算,前提是数组长度必须是2的幂。如果发生哈希冲突,就尾插法追加到链表尾部(注意JDK 7是头插法,头插法在并发扩容时可能形成环,这就是HashMap线程不安全的一个典型原因)。当元素数量超过阈值loadFactor*capacity时,触发扩容,默认负载因子0.75,容量翻倍。
第三个高频考点:为什么HashMap线程不安全。你只需要记住两个场景:JDK 7并发扩容可能成环,导致get死循环;JDK 8虽然改成尾插法解决了成环问题,但多线程put时可能出现数据覆盖,因为put是check-then-act操作,多个线程同时判断某个桶为空,然后同时赋值,后写的会覆盖先写的。
ConcurrentHashMap则是另一个必考考点。JDK 7用Segment分段锁,继承ReentrantLock,理论上支持Segment数组大小的并发度。JDK 8放弃分段锁,改用CAS加synchronized锁住桶的头节点,并发粒度更细。它不允许key或value为null,这一点和HashMap不同,原因是为了避免并发场景下的二义性:如果get返回null,你没法判断是key不存在还是value本身是null,在并发环境下这种不确定性是有隐患的。
2.2 异常体系与try-with-resources
异常相关的题在试卷B里通常是选择题,但有一类比较坑:给你一段代码,问输出什么,或者问finally里的代码是否一定执行。
先理清基本概念。Java的异常是Throwable的两个分支:Error和Exception。Error是JVM层面的严重错误,比如OutOfMemoryError、StackOverflowError,应用程序不应该去捕获。Exception下面又分受检异常和非受检异常。受检异常必须显式捕获或向上抛,典型的是IOException、SQLException;非受检异常继承RuntimeException,可以不做处理,典型的是NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException。试卷里经常让你区分一个异常到底属于哪一类。
关于finally,有一道经典题的答案你必须背下来:finally块中的代码并不是一定执行。以下情况不执行:
- 在try或catch中调用了System.exit()终止JVM;
- JVM崩溃或断电等极端情况;
- 在try块中没有正常执行到finally之前,比如陷入了死循环。
另一种考察方式是finally中的return会覆盖try中的return。如果try里先返回一个值,finally里又有一个return,最终返回的是finally里的值。这个细节很容易被忽略,我建议你最好自己写个例子跑一遍,比死记结论要牢靠得多。
try-with-resources是JDK 7引入的资源自动关闭机制,凡是实现了AutoCloseable接口的类,都可以放在try后面的圆括号里,代码块执行完后资源会被自动关闭。它比传统的手动finally关闭更安全,因为即使代码块内抛异常,关闭资源时如果也抛异常,原始的异常会被保留。笔试时如果出现资源关闭相关的代码,优先考虑用try-with-resources。
2.3 Lambda、Stream与枚举的冷门考点
Lambda和Stream在试卷B里不会考得太深,但经常以高频选择的形式出现。有一个容易错的知识点:Lambda表达式实际上是对函数式接口的实例化。所谓函数式接口,就是只包含一个抽象方法的接口,比如Runnable、Comparator、Callable,以及java.util.function包下的Function、Predicate、Supplier、Consumer。判断一个接口是不是函数式接口,可以看它有没有加@FunctionalInterface注解,默认方法、静态方法不会破坏函数式接口的定义。
Comparator.comparing这个用法在笔试编程题和简答题里出现过。它的核心逻辑是让你指定一个提取key的函数,然后按照key对元素排序,比如
list.sort(Comparator.comparing(User::getAge))
。如果你想把某个值排到最前面,可以用
Comparator.comparing(User::getAge).thenComparing(...)
来实现多重排序规则,或者用一些技巧把特殊的key映射为优先值。这里提醒一点:排序如果是倒序,注意null值的处理,Java 8的Comparator默认对null不友好,需要用到Comparator.nullsLast来处理。
枚举也是高频考点。它可以定义字段、构造方法、抽象方法,可以配合switch使用。有一个经典问题是:为什么枚举能实现单例?因为枚举类的构造器是私有的,且JVM层面保证了每个枚举常量只会被实例化一次,同时天然支持序列化,不会因为反序列化创建新的实例。这一点比双重检查锁定的单例实现更安全,笔试问"最推荐的单例写法"时,答案就是枚举单例。
Stream的常见操作里,map、filter、collect、sorted是必会的基础。容易被考到的是peek与map的区别:peek是中间操作,接收Consumer,不改变元素,而map是转换操作。还有一个点是parallelStream,它不是万能加速器,对于共享可变状态的操作反而可能出错,笔试如果问性能问题,一定要提到线程安全因素的考量。
3. JVM与并发:内存溢出、GC与线程安全的组合拳
3.1 各种OOM的成因区分,别只会说内存不够
关于JVM内存结构,试卷B的简答题几乎必考。你需要把运行时数据区说得清清楚楚:堆、虚拟机栈、本地方法栈、程序计数器、方法区(JDK 8后是元空间)。其中堆是对象分配的主要区域,虚拟机栈是执行Java方法时创建栈帧,程序计数器是当前线程执行的字节码行号指示器。
试卷里经常出现一个很具体的报错,比如最原始的题目可能是给你一段OutOfMemoryError相关日志,问这个错误是什么原因。常见的OOM可以分成这么几类:
| 报错信息 | 发生区域 | 主要原因 |
|---|---|---|
| Java heap space | 堆 | 对象太多或对象过大,堆内存不足 |
| GC overhead limit exceeded | 堆 | GC频繁执行但回收效果差,JVM自我保护 |
| Metaspace | 元空间 | 加载的类过多或动态生成类膨胀 |
| unable to create new native thread | 操作系统层面 | 线程数超过系统上限 |
| insufficient memory | 本地内存 | 操作系统无法提供足够的内存给JVM |
特别注意"insufficient memory"这个报错。它不是常见的堆内存溢出,而是JVM向操作系统申请本地内存失败。什么叫本地内存?比如JVM需要分配线程栈、DirectByteBuffer对应的堆外内存、JIT编译器相关的内存、元空间,这些都算。如果你看到"java: OutOfMemoryError: insufficient memory",优先怀疑几个方向:物理内存本身不够、进程的虚拟内存地址空间耗尽、cgroup或容器限制导致的内存配额不足、以及换页空间不足。
排查这类问题的思路:先
free -m
看物理内存,再
top
看JVM进程内存占用,
jmap -heap
看堆内情况,
jstat -gcutil
看GC压力。如果堆内存使用率不高但进程整体内存持续上涨,就要怀疑堆外内存泄漏,比如DirectByteBuffer没有释放。我见过一个网关服务频繁OOM的案例,最后定位到是因为堆外缓存使用了大量DirectMemory,又没有注意回收,导致操作系统层面的内存被耗尽。
3.2 线程池的核心参数与执行流程
线程池的考察在途虎这类笔试中基本是必考的,形式可能是简答题也可能是选择题。你至少要能准确说出ThreadPoolExecutor的7个核心参数,外加解释阿里的规范"不要用Executors创建线程池"。
7个参数:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。流程是这样的:
- 提交任务后,如果当前线程数小于核心线程数,创建新线程执行任务;
- 如果线程数大于等于核心线程数,任务先进入阻塞队列;
- 如果队列满了,继续创建线程,直到达到最大线程数;
- 如果线程数已经达到最大值,就触发拒绝策略。
这里要强调的是队列选择对业务的影响。LinkedBlockingQueue无界队列会导致最大线程数形同虚设,任务无限在队列里堆积;SynchronousQueue不缓存任务,来一个任务就必须创建一个新线程,适合任务量小但要求快速响应的场景;ArrayBlockingQueue有界队列是最常用的。另一个细节:corePoolSize和maximumPoolSize之间的线程,在空闲时间超过keepAliveTime后会被回收。
4种拒绝策略:AbortPolicy直接抛异常(默认)、CallerRunsPolicy由调用线程执行、DiscardPolicy丢弃任务、DiscardOldestPolicy丢弃队列中最旧的任务。如果在途虎这种秒杀活动场景下设计线程池,建议用有界队列加CallerRunsPolicy,因为抛异常会导致接口直接失败,而由调用线程执行可以起到降级和限流的效果。
阿里的规范为什么要禁掉Executors?两个原因。Executors.newFixedThreadPool用的是无界的LinkedBlockingQueue,任务堆积可能导致内存溢出;newCachedThreadPool的maximumPoolSize是Integer.MAX_VALUE,队列是SynchronousQueue,并发量大时会创建大量线程,同样会造成OOM或者线程资源耗尽。所以生产环境必须手动new ThreadPoolExecutor,把参数暴露出来进行可控配比。
3.3 synchronized、ReentrantLock、volatile与ThreadLocal
并发编程部分的考点相对密集。synchronized和ReentrantLock的区别,是每一家公司的笔试题里都有的常客。你能答出两者都是可重入的、都可以保证可见性和原子性,已经很不错了。但答出以下差异就更加分:
- synchronized是隐式锁,自动获取和释放;ReentrantLock是显式锁,需要lock和unlock,且unlock必须放在finally中。
- ReentrantLock支持公平锁和非公平锁,synchronized只有非公平锁。
- ReentrantLock可以中断等待、可以设置超时(tryLock),synchronized不能。
- ReentrantLock支持多个Condition,synchronized只有一个监视器锁。
- JDK 6之后synchronized引入了偏向锁、轻量级锁、重量级锁的升级过程,所以性能不见得比ReentrantLock差,在低竞争环境下synchronized表现更优。
volatile是一个高频考点。它保证两点:可见性和有序性(禁止指令重排序),但不保证原子性。典型例子是"volatile修饰的计数器在多线程下i++仍然不安全",因为i++是读取-修改-写入三步操作。在场景设计题里,你如果提出用volatile保证并发安全,面试官很可能追问原子性问题,这是一个容易暴露深浅的点。
ThreadLocal的考点主要是两个:每个线程都有独立的变量副本,以及它的内存泄漏问题。ThreadLocalMap的Entry继承了WeakReference,key是弱引用,但value是强引用。如果ThreadLocal对象没有被显式remove,而线程仍然存活,那么key可以被回收但value无法被回收,接下来key变成null,value却一直存在,导致内存泄漏。正确的做法是在finally块里调用remove。在线程池场景下使用ThreadLocal尤其危险,因为线程会被复用,一个请求设置的值可能在另一个请求中被读到,这就是跨请求数据串扰,必须在使用完立即清理。
死锁相关问题在试卷里一般是问你"如何避免死锁"。回答思路是:破坏四个必要条件之一——互斥(无法破坏,锁本身就是为了互斥)、持有并等待(可以采用一次性申请所有锁)、不可剥夺(可以设置超时,tryLock)、循环等待(可以保证加锁顺序一致)。笔试写代码时,最实用的手段就是所有线程都按照同一个顺序获取多个锁。
4. 框架与中间件:业务系统开发能力的试金石
4.1 Spring Boot自动配置与MyBatis防注入
试卷B在框架题上侧重的是主流实用型的考察,Spring Boot的自动配置原理是我的建议重点。你要能说清楚:Spring Boot通过
@EnableAutoConfiguration
注解,导入AutoConfigurationImportSelector,利用SpringFactoriesLoader从
META-INF/spring.factories
或
AutoConfiguration.imports
文件中加载所有候选的自动配置类,再通过
@ConditionalOnClass
、
@ConditionalOnMissingBean
这类条件注解决定哪些配置生效。简而言之,自动配置就是"根据classpath下的依赖和配置属性智能地组装Bean"。
MyBatis有一个经典问题:
#{}
和
${}
的区别。前者是预编译占位符,传入的值会被当作参数传给PreparedStatement,由JDBC驱动进行转义,能够有效防止SQL注入;后者是字符串直接拼接,相当于把变量内容直接拼进SQL语句里,存在注入风险。凡是涉及order by、表名这类无法使用参数占位符的场景,必须对传入内容做白名单校验,更不应该直接把用户可控的字符串拼进SQL。
除此以外,MyBatis一级缓存是SqlSession级别的,默认开启;二级缓存是namespace级别的,需要显式配置。在分布式环境下,二级缓存可能出现数据一致性问题,所以并没有无脑开启为佳。这些细节很可能出现在选择题或简答题的加分项里。
4.2 MySQL索引与事务隔离级别
MySQL在途虎这类重度依赖数据库存储的系统中是核心中的核心。订单要查、门店要查、库存要查、用户要查,几乎所有业务都离不开查询,所以索引相关题目必考。
先理清B+树索引的特性:它是多路平衡搜索树,非叶子节点只存索引键,叶子节点存全量数据(聚簇索引)或主键值(二级索引)。这种结构天然适合范围查询、排序操作,因为叶子节点之间用双向链表相连,一次范围扫描不需要回根节点反复查找。
聚簇索引和非聚簇索引的区别是:InnoDB的主键索引就是聚簇索引,数据行物理存储在叶子节点;二级索引的叶子节点存储的是主键值,所以通过二级索引查找数据时需要"回表"。这时候覆盖索引就能派上用场:如果查询的列恰好都在二级索引里,就不需要回表,直接返回索引数据,性能提升很明显。最左前缀原则说的是联合索引的匹配规则,比如建立(a,b,c)联合索引,查询条件里有a和b能用到索引,只有b和c就用不上。
事务隔离级别这块,四个级别你必须背得滚瓜烂熟:读未提交、读已提交、可重复读、串行化。InnoDB默认是可重复读。由此引入三个概念:脏读(读到未提交数据)、不可重复读(同一查询条件下,第二次读到的数据被其他事务修改了,但字段值一样)、幻读(第二次读到的行数变多了)。MVCC机制用undo log版本链和readView实现一致性快照读,使得在可重复读级别下,同一个事务内多次查询能看到一致的数据快照。
笔试常见的坑是:可重复读级别下,普通快照读不会出现幻读,但当前读(select ... for update、update、delete)仍可能发生幻读。所以InnoDB引入了间隙锁和next-key lock来配合解决,但这块在笔试里只要点到即可,面试时才需要详细展开。
4.3 Redis缓存与分布式锁的答题思路
Redis相关题在试卷B里的存在感很强,尤其是缓存穿透、缓存击穿、缓存雪崩这三个概念,几乎是简答和场景题的钉子户。
- 缓存穿透:查询一个不存在的数据,缓存里没有,请求一直打到数据库。解决思路:缓存空值,为不存在的数据设置一个较短的过期时间;或者使用布隆过滤器,在缓存之前先判断key是否存在。
- 缓存击穿:某个热点key过期,大量并发请求同时去打数据库。解决思路:互斥锁,让同一时刻只有一个线程去重建缓存;或者逻辑过期,让热点key的逻辑过期时间短暂延长,后台异步刷新。
- 缓存雪崩:大量key同时失效,或者Redis宕机,导致数据库被打爆。解决思路:过期时间加随机值,避免key在同一时间过期;做多级缓存;Redis集群高可用方案;限流降级。
分布式锁怎么答?一句话概括:用Redis的SETNX加EXPIRE原子命令,或者用Redisson看门狗机制实现可续期的锁。注意一个关键细节:不能分两步执行SETNX和EXPIRE,否则进程在设置锁之后、设置过期时间之前挂了,锁就永远不会释放。应该是
SET lockKey value NX PX 30000
,一条命令完成。释放锁时要用Lua脚本比较value是否一致再删除,避免误删其他线程的锁。这个场景在途虎的营销活动、优惠券系统、订单状态更新中非常常见。
4.4 API安全对接与ES异步写入的实务延伸
从热搜词的观察来看,Java Spring Boot的API Key安全对接、ES异步写入这类偏实战的问题,也很常出现在面试追问里。
API Key的通用方案是:客户端调用服务端接口时,header中带上AppId和Key,服务端用拦截器(HandlerInterceptor)或过滤器(OncePerRequestFilter)校验。更安全的做法是加签名:把业务参数和时间戳拼接成字符串,用约定密钥做HMAC-SHA256签名,服务端用同样的密钥重放签名并比对,同一时间戳短时间内允许重复请求且过期时间校验通过,就能防止重放攻击。
ES的异步写入在Java里通常配合消息队列来实现:业务数据先写入MySQL,同时发送一条消息到Kafka/RabbitMQ,消费者从MQ拉取消息后异步写入ES,这样既不影响主链路时延,也能在ES写入失败后重试。一旦写入失败重试次数达到上限,可以丢进死信队列人工排查。这套思路在途虎这种既有结构化订单数据、又有大量日志/搜索需求(门店搜索、商品搜索)的场景里是很典型的通用方案。
5. 算法编程题:排序、链表与场景设计的实战节奏
5.1 排序算法的边界条件与复杂度
试卷B的编程题难度大概在LeetCode中等偏下,不会出特别变态的题,但这不意味着你可以轻视。最常见的排序题就是冒泡排序和快速排序。
冒泡排序虽然简单,但笔试时有个优化点:某一轮遍历如果没有任何交换,说明数组已经有序,可以直接退出。加上这个标志位,在接近有序的数组上可以降到接近O(n)的时间复杂度,这是区分你有没有真实写过代码的细节。
快速排序是另一个高频考点。它基于分治:选一个基准元素,把数组分成小于基准和大于基准两个部分,再对子数组递归排序。最坏情况发生在每次基准都选到最大值或最小值时,时间复杂度退化为O(n²),所以实际应用中常用三数取中法或者随机选取基准来避免这种情况。平均时间复杂度O(nlogn)。
我贴一个笔试风格的快排实现,注意边界条件:
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 pivotValue = arr[left];
int i = left, j = right;
while (i < j) {
while (i < j && arr[j] >= pivotValue) j--;
arr[i] = arr[j];
while (i < j && arr[i] <= pivotValue) i++;
arr[j] = arr[i];
}
arr[i] = pivotValue;
return i;
}
笔试里如果只要求排序,先考虑Arrays.sort()能不能用;如果要求手写排序,快速排序和归并排序选一个练熟。归并排序还额外适合解决逆序对问题,必要时可以用。
5.2 链表和二分查找的代码注意事项
链表相关的题在试卷里出现过多次,最常见的是反转链表、环形链表检测、删除倒数第N个节点。
反转链表用迭代法写最稳:三个指针,prev、curr、next,每次把curr.next指向prev,然后整体后移。边界条件是链表为空或只有一个节点。代码量很小,但很多人会在最后忘记把头节点指向正确位置,建议先在纸上画一遍指针变换。
环形链表检测的标准解法是快慢指针:fast每次走两步,slow每次走一步,如果存在环,两者必然相遇;如果fast走到null,说明无环。这题的证明关键是:环入口之前的路程加上环内步数存在模运算关系,笔试不会让你严格证明,但你要能表达清楚"快慢指针在环内一定相遇"。
二分查找是笔试最常见的"看似容易但容易写错"的题目,核心是循环条件
left <= right
以及每次收缩区间时
mid
的更新。经典写法:
int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
注意
mid = left + ((right - left) >> 1)
这个写法,它可以防止left+right整数溢出,笔试和面试中提到这一点会显得你考虑周全。还有就是边界条件的取值:如果循环是
left < right
,通常配合的是区间左闭右开,容易搞混,建议全篇统一用
<=
+左闭右闭,减少心智负担。
5.3 结合业务的场景设计题怎么答
试卷B的场景设计题非常务实,几乎都是围绕互联网业务系统的常见问题。比如:
- 如何设计一个订单超时自动关闭系统?
- 如何设计一个秒杀场景下的库存扣减方案?
- 途虎预约到店场景,怎么避免同一个时间段被重复预约?
回答这类题有一个通用框架:先明确核心诉求,再说数据存储选型和关键流程,最后补充异常和并发处理。
以订单超时关闭为例。最简单的方案是起一个定时任务,每隔一段时间扫描数据库中超时的订单,批量更新状态。这个方案的优点是实现简单,缺点是扫描范围大、对数据库有压力,而且存在时间窗口误差。更优的方案是用延迟队列:订单创建时把订单ID放到延时队列,延迟时间到后,消费者取出订单并判断是否已支付,如果未支付则关闭订单。可以用RabbitMQ的延迟消息插件,也可以用Redis的过期key配合监听,或者直接用Redisson的延迟队列。答题时提到"用延迟队列替代定时轮询"这个优化方向,通常能拿到不错的分数。
库存扣减场景要注意两个点:一是防止超卖,二是防止数据库压力过大。超卖的经典解法是条件更新:
UPDATE stock SET count = count - 1 WHERE goodsId = ? AND count > 0
,减少的话影响行数为0就说明库存不足。如果要缓解数据库压力,可以先用Redis的incr/decr原子操作预扣减库存,再异步同步到数据库。答题时把这两层讲清楚,就比只写"加乐观锁"要翔实得多。
预约时间冲突的场景,本质是区间重叠判断。数据库层面可以在门店、时间段字段上建联合唯一索引,也可以采用"提前锁定时间段+事务内检查+唯一约束"的双保险设计。笔试时能把思路说清楚,比写出完整代码更得分。
6. 笔试现场避坑:环境配置、编译错误与时间分配
6.1 本地环境与在线编译器的经典报错
笔试过程中很多人不是挂在题目上,而是挂在环境上,这一点经常被忽略。几个实测概率很高的报错值得提前说道说道。
Lombok相关报错:编译时提示
java: You aren't using a compiler supported by lombok, so lombok will not work
。这个报错通常在JDK版本和Lombok版本不匹配的时候出现。比如你本地用的JDK 21,但项目依赖的Lombok还是1.16系列。解决方案:升级Lombok版本到1.18.30以上,或者降低JDK版本。在笔试在线环境中大概率不会遇到,但如果用本地IDE写练习题,这类问题很常见,提前把环境调好能省下大把时间。
另一个高频报错:
java: 警告: 源发行版 17 需要目标发行版 17
(或者反过来,目标发行版低于源发行版)。这说明编译器使用的JDK版本和项目target版本不一致。在IDEA里需要检查File - Project Structure - Project SDK和Modules里的Language Level,在Maven项目里则需要检查pom.xml中maven.compiler.source和maven.compiler.target是否一致。最好在
<properties>
里统一配置:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
这样Maven在编译时会沿用指定的源码和目标版本,避免IDE默认配置覆盖引起混乱。
还有就是数组越界异常,我记得热搜词里也有这个。写代码时很多越界都发生在循环边界上,比如
for (int i = 0; i <= arr.length; i++)
,应该用
i < arr.length
。笔试时尽量在循环条件里统一写成
<
,并且对数组访问前做空数组判断。这种边界错误在在线判题系统里会直接导致运行时错误,AC不了题。
6.2 答题时间分配的实战策略
笔试时间通常在90到120分钟之间,题量大概40到60道,要合理分配时间,我的建议是:
- 前10到15分钟,快速做完选择题和判断题。遇到拿不准的题不要死磕,先按直觉选一个并标记,等后面有时间再回头。
- 接着用20分钟左右做简答题。不用写长篇大论,踩点作答,分条列点写清楚。
- 数据库和SQL题用15分钟左右,注意审题,尤其是多表关联和索引设计的细节。
- 最后留出40到50分钟做编程题,这是拿分大头。如果编程题不止一题,先做自己最有把握的那道,保证至少AC一道,再考虑第二道。
- 场景设计题如果留在最后,至少要把思路和核心方案写出来,不要留白。
特别是编程题,在线编程平台(牛客、赛码网)的输入输出格式有差异。有的平台要自己写Scanner解析标准输入,有的平台是函数输入输出。我的建议是去牛客上练几道io练习题,把Scanner和BufferedReader两种读法都提前跑一遍,避免正式笔试时因为IO处理卡壳。
另外,在线判题平台对时间复杂度和空间复杂度有限制,如果算法思路对了但还是超时,考虑是否需要改用更高效的数据结构,而不是死磕同一个算法。有一道题要特别注意:如果输入规模在10^5级别以上,O(n²)的暴力解法基本上会超时,这时就得考虑二分、哈希表或者双指针优化。
途虎这套试卷B还有一个特点:题目题干往往比较长,会先描述一个业务场景,再问你实现方案。读题时先抓关键词,"并发""一致性""性能""可用性"这些词出现的位置,决定了你答题的侧重点。不要被场景中的多余信息干扰,本质上考点就在几个固定领域里。
刷完这套卷,我个人最大的感受是:它不像一些大厂卷子那样追求偏题怪题,而是在反复考察基础功底的深度和业务场景的敏感度。如果你在准备途虎的秋招,或者把途虎笔试当练手,建议把本文提到的几个重点模块——集合、并发、JVM内存、MySQL索引与事务、Redis缓存、线程池——逐项夯实,每块都能说出原理且能上手写代码,笔试通过的概率会高出很多。最后分享一个我自己用过的小技巧:把每套试卷里做错的题整理成一份带错因分析的笔记,考前只看笔记里的错题和对应的知识点导图,比重新刷题效率高得多。
1017




被折叠的 条评论
为什么被折叠?



