每年这个时候,都有学弟学妹来问我:“爱奇艺的Java笔试难不难?第二场和第一场有什么区别?”我当年也是被这套题折磨过的人,翻出当时的记录看了看,发现很多考点放到现在依然是校招笔试的主流方向。今天就以这套爱奇艺2020校招Java方向笔试题(第二场)为线索,把Java方向校招笔试常考的核心知识点、真题思路和备考方法完整拆一遍,希望能给准备校招或者刚入行想查缺补漏的朋友一点参考。
这套题适合谁?不只是投爱奇艺的同学,只要是目标是互联网中大型公司的Java后端岗,都值得做一遍。因为这类公司的笔试题目构成高度相似:选择题死磕Java基础、集合、并发、JVM,编程题考查算法基本功和代码落地能力。准备过这套题,基本就掌握了这类笔试的通用打法。
1. 从爱奇艺第二场笔试题看校招Java笔试的出题逻辑
1.1 为什么爱奇艺这类公司偏爱“基础+工程场景”混合出题
视频平台后端Java工程师要处理的核心问题是什么?大体上是围绕海量用户访问的内容推荐、播放鉴权、评论弹幕、会员订单等接口开发。这些业务场景天然带有高并发、高可用、大数据量的特征,所以笔试题不会只考死记硬背的语法点,而是把基础知识和业务场景强行揉在一起。
比如同样是考HashMap,纯背“数组+链表+红黑树”只是及格水平,它们更想看到的是:你能不能说出扩容阈值为什么是0.75、为什么链表长度到8才转红黑树、并发环境下HashMap为什么会出现死循环。选择题里如果出现“以下哪个容器是线程安全的”这种题,背后就是在考察你能不能在实际项目里正确选型。
第二场和第一场在难度梯队上通常有一点差异,但知识范围不会跳出JavaSE主线。如果按知识点出现频率排序,大致是这样:
- Java基础语法与API:String、异常、包装类、枚举
- 集合框架:HashMap、ArrayList、LinkedList、HashSet
- 并发编程:线程状态、synchronized、volatile、线程池
- JVM:内存区域、垃圾回收、类加载机制
- 输入输出与网络:IO/NIO、TCP、HTTP
- 算法编程题:字符串、数组、链表、动态规划
这个顺序本身也反映了出题人的思路:先确认你有没有扎实的语言功底,再看你对并发和虚拟机的理解,最后用算法题检验你的代码落地能力。
1.2 笔试的通过逻辑:不是要满分,是要排序靠前
很多同学备考时有一个误区,觉得笔试要尽量拿高分。实际上互联网大厂笔试本质是“筛选排序”,不是“及格考”。同样一套题,有人选择题全对但编程题A不出来,有人选择题错了一半但两道编程题全过,后者在简历筛选阶段往往更占优势。
这里的逻辑是:选择题只能筛掉基础不牢的人,编程题才是真正区分度的关键。因为日常工作里,语言基础差可以靠查文档补,但算法逻辑和代码实现能力很难短期补齐。所以我的一个核心建议是:如果时间有限,优先保证编程题能写出高正确率的暴力解,再回头抠选择题的边角知识,这比抱着面试题背一整天收益高得多。
爱奇艺这套第二场笔试我记得选择题大概有20来道,编程题2道左右,总时长比较紧张。这意味着每一道选择题不能死磕超过两分钟,编程题至少留出40分钟。时间分配比知识储备更容易被忽略,但往往决定最终结果。
2. Java基础与集合考点:历年笔试题的“基本盘”
2.1 String不可变性、StringBuilder与StringBuffer的取舍
选择题里如果考String相关,最常见的出法是给一段代码让你判断创建了多少个对象,或者比较两个引用是否相等。这里要注意的点其实是三个:字符串常量池、不可变性、不同字符串拼接方式的性能差异。
String s1 = "abc"; String s2 = new String("abc"); System.out.println(s1 == s2); // false System.out.println(s1.equals(s2)); // true这题的考点在于:s1直接指向常量池里的“abc”,而s2在堆上创建了新对象,两个引用当然不相等。但如果写成String s3 = s2.intern(),那s3和s1就指向同一个常量池对象了。笔试里只要看到new String,第一个反应就应该是“常量池里创建了一个对象,堆里又创建了一个对象”,除非原本常量池里还没有这个字符串。
关于StringBuilder和StringBuffer,大多数人只记得“前者非线程安全、后者线程安全、性能前者好”,但笔试经常反着考:在单线程环境下哪个更快,在循环拼接字符串时直接用+性能如何。答案就是字符串常量池模式下,+在循环中会被优化成StringBuilder.append,但如果循环外先创建了StringBuilder,循环内反复sb.append()反而更高效。这个点不去查字节码很容易掉坑。
我的建议是备考时可以直接把String类源码看一遍,重点看value数组为什么用final修饰、intern()方法干了什么,这两块看懂了选择题基本不用背。
2.2 HashMap、ArrayList、LinkedList底层原理对比
集合框架是Java笔试的绝对核心,几乎每场笔试都会有一到两道关于HashMap的题。当年爱奇艺这套笔试选择题里也出现了类似考点,出题角度一般是底层结构、size扩缩容、并发问题。
HashMap在JDK1.8里的底层逻辑是:数组加链表加红黑树。默认初始化容量16,负载因子0.75,当元素个数超过容量 * 负载因子时触发扩容,每次扩容翻倍。链表长度达到8且数组长度达到64时链表转红黑树,树节点数降到6时退化为链表。
这些数字属于典型的“背了没用但必须背”的内容,因为选择题直接就是填空式考法。不过更值得理解的是为什么这么设计:负载因子0.75是空间利用率和冲突率之间的折中,太大容易频繁冲突,太小浪费空间;为什么是8,是因为泊松分布下,负载因子0.75时链表长度达到8的概率已经非常低,用红黑树是为了预防极端hash冲突,保证最坏情况下也能维持对数级查找。
ArrayList和LinkedList的对比也是高频题。记住一条主线就够了:ArrayList基于动态数组,随机访问O(1),中间插入删除O(n);LinkedList基于双向链表,头尾插入删除O(1),随机访问O(n)。笔试选择题经常给一个操作场景,问哪个容器更合适。判断标准很简单:大量随机读取选ArrayList,频繁头尾插入删除选LinkedList,中间插入两者都没好下场。
2.3 构造方法、初始化块、继承的执行顺序
Java类加载与初始化顺序属于那种“平时写代码根本遇不到,但笔试乐此不疲”的考点。给出一个父类和子类,各带静态代码块、实例代码块、构造方法,让你判断输出顺序。这类题的规律非常固定:
- 父类静态代码块
- 子类静态代码块
- 父类实例代码块
- 父类构造方法
- 子类实例代码块
- 子类构造方法
只要记住“先静态后实例、先父后子、静态只执行一次”这个口诀,大部分题都能应对。容易翻车的地方在于子类实例化时,如果子类构造方法第一行没显式调用super(),会默认调用父类无参构造,如果父类没有无参构造且子类又没传参,编译直接报错。
这类题在笔试里没有工程意义,但能反映出你是否真正理解对象创建的生命周期,所以几乎每一家大厂都会出。
2.4 异常体系与try-catch-finally的细节
异常考点主要集中在三处:受检异常和非受检异常的划分、finally与return的执行顺序、自定义异常的使用。
finally与return的组合题是最常见的坑。很多人以为finally里的return会覆盖try里的return,其实在Java里finally块如果包含return,它会覆盖try或catch中的返回值,但更值得记住的是:如果try里是return a++,finally里再修改a,最终返回值是修改前还是修改后的值。答案是返回的是修改前的值,因为return a++先把a的值压入操作数栈,finally只是修改了局部变量a,栈里的返回值不会变。
这类题我会建议直接手写几个例子跑一遍,而不是死记结论,因为出题人会变着法考。比如try里有System.exit(0)时finally不会执行,这个细节也偶尔出现。
3. 并发与JVM考点:拉开分数的分水岭
3.1 volatile、synchronized、Lock的区别不能只背结论
并发是Java笔试里最容易拉开差距的部分,因为考点不局限于语法,还要看你对计算机内存模型的理解。选择题常考的就是volatile和synchronized三组对比:
- volatile保证可见性和有序性,不保证原子性;synchronized同时保证三者
- volatile不能修饰final变量,synchronized可以修饰方法、代码块、静态方法
- synchronized在JDK1.6之后有偏向锁、轻量级锁、重量级锁的升级过程;Lock是显式锁,支持可中断、可超时、公平锁
笔试如果出“volatile能否保证原子性”,答案是“不能”。比如volatile int count,多线程并发执行count++,最终结果仍然可能小于预期值,因为“读取-修改-写入”三个步骤本身不是原子的。而如果题目换成“volatile能否保证可见性”,答案是“能”,因为它强制线程从主内存读取最新值。
如果你想在笔试里写出加分答案,可以在选择题解析里补充一个点:从Java内存模型来看,volatile通过内存屏障禁止指令重排序,synchronized通过管程Monitor实现互斥和可见性。这两个概念会让面试官觉得你不只是背了结论。
3.2 线程池的核心参数是笔试常客
线程池是并发部分几乎必考的知识点。爱奇艺这套题以及同期其他大厂的Java笔试里,最常见的是给出一段创建线程池的代码,问你核心线程数、最大线程数、队列容量对任务执行的影响。
这里有一个清晰的记忆框架:任务来了先交给核心线程;核心线程满了放进阻塞队列;队列满了再创建非核心线程直到最大线程数;再满了就执行拒绝策略。四个参数环环相扣,任何一道选择题都逃不出这个链路。
以下是高频选择题中容易混淆的参数:
| 参数 | 作用 | 笔试陷阱 |
|---|---|---|
| corePoolSize | 核心线程数,常驻线程 | 默认不回收,除非allowCoreThreadTimeOut |
| maximumPoolSize | 最大线程数 | 必须大于等于corePoolSize |
| workQueue | 任务队列 | 有界队列和无界队列策略不同 |
| keepAliveTime | 非核心线程空闲存活时间 | 只作用于超出核心线程数的线程 |
拒绝策略有四种:AbortPolicy直接抛异常、CallerRunsPolicy由调用线程执行、DiscardPolicy直接丢弃、DiscardOldestPolicy丢弃最老任务。笔试选择题如果问“生产环境默认推荐哪种”,其实是问“哪种策略最安全但影响用户体验”,答案基本倾向AbortPolicy加上自定义告警。
3.3 JVM内存区域与垃圾收集器的经典选择题
JVM部分的出题风格非常固定:内存区域划分、垃圾回收算法、类加载双亲委派这三板斧。其中内存区域划分是最基础的一题,经常直接考“以下哪个区域不会发生OutOfMemoryError”,答案是程序计数器。
JVM运行时数据区包括:
- 程序计数器:线程私有,记录当前线程执行的字节码行号,唯一不会OOM的区域
- 虚拟机栈:线程私有,存放栈帧,栈深度不够时抛StackOverflowError
- 本地方法栈:线程私有,服务native方法
- 堆:线程共享,存放对象实例,GC主要区域
- 方法区(JDK1.8后元空间):线程共享,存放类信息、常量、静态变量
垃圾收集器的选择部分,笔试很少深问G1的RSet结构,更多是问“新生代用什么算法”“老年代用什么算法”。新生代用复制算法,优点是不会有内存碎片;老年代用标记整理或标记清除,其中CMS是标记清除但会产生碎片,G1是标记整理加局部复制。背住这一条主线,选择题基本就稳了。
双亲委派机制也是热门考点:类加载器从下往上委托,父加载器能加载就不让子加载器加载。好处是避免核心类被重复加载和篡改。如果考到“如何打破双亲委派”,答案是继承ClassLoader并重写loadClass方法,像JDBC和Tomcat都这么干过。
3.4 从视频业务角度看为什么考JVM并发
有人会问:“我去爱奇艺写业务,为什么不考Spring Cloud却考JVM?”这件事得反过来想:做视频平台后端的典型场景是大量用户同时请求播放地址、同时发表弹幕,如果工程师不理解和锁和线程池,处理高并发接口时连排查死锁的思路都没有。
比如一个播放鉴权接口的QPS很高,如果用synchronized锁住整个方法,别人慢慢排队,响应时间会拉长;如果改为锁粒度更细的分布式锁或者写乐观锁重试,就能明显改善吞吐。类似这种思路不是八股,而是业务场里实实在在要面对的问题。笔试阶段考并发和JVM,就是在筛人进来后放在真实业务里的存活率。
4. 编程题实战:从读题到AC的完整思路
4.1 典型算法题:合并区间(变体)
爱奇艺这一场笔试题里出现的编程题,我印象比较深的一道是合并区间类的问题。题目大概描述是:输入若干区间,合并所有重叠区间并输出。这题在LeetCode上能搜到原题,力扣56题,但笔试时会做一点变形,比如输入是二维数组,或者要求输出合并后的区间个数。
解题思路是惯用的排序加贪心:
- 将所有区间按左端点升序排序
- 维护当前合并区间的左端点和右端点
- 遍历区间:如果当前区间左端点大于维护的右端点,说明不重叠,把之前区间加入结果;否则取较大的右端点继续合并
public int[][] merge(int[][] intervals) { if (intervals.length == 0) return new int[0][]; Arrays.sort(intervals, (a, b) -> a[0] - b[0]); List<int[]> merged = new ArrayList<>(); int left = intervals[0][0], right = intervals[0][1]; for (int i = 1; i < intervals.length; i++) { if (intervals[i][0] <= right) { right = Math.max(right, intervals[i][1]); } else { merged.add(new int[]{left, right}); left = intervals[i][0]; right = intervals[i][1]; } } merged.add(new int[]{left, right}); return merged.toArray(new int[merged.size()][]); }这道题容易丢分的地方在于边界条件:空数组、只有一个区间、区间左端点相同但右端点不同。笔试系统不会给你调错机会,所以写完最好自己在草稿纸上模拟一遍。
时间复杂度的计算也是得分点:排序O(n log n),遍历O(n),总复杂度O(n log n)。如果选择题里问空间复杂度,答案是结果列表占用的O(n)额外空间。
这类题为什么被反复出?因为它考察的是有序思维、边界处理和基础排序,这些恰好是数据结构课程里最值钱的部分,比一些花哨的动态规划更有工程意义。
4.2 高频多线程手写题:三个线程循环打印ABC
另一类高频率出现的编程题是手动实现多线程协作,比如“三个线程循环打印ABC十次”。有些笔试系统把这类题归为选择题,给几个实现让你判断输出结果,但也有线上笔试直接要求手写代码。
最推荐的做法是用ReentrantLock加Condition控制顺序:
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class PrintABC { private static ReentrantLock lock = new ReentrantLock(); private static Condition condA = lock.newCondition(); private static Condition condB = lock.newCondition(); private static Condition condC = lock.newCondition(); private static int state = 0; public static void main(String[] args) { Thread ta = new Thread(() -> print("A", 0, condA, condB)); Thread tb = new Thread(() -> print("B", 1, condB, condC)); Thread tc = new Thread(() -> print("C", 2, condC, condA)); ta.start(); tb.start(); tc.start(); } private static void print(String name, int target, Condition cur, Condition next) { for (int i = 0; i < 10; i++) { lock.lock(); try { while (state % 3 != target) { cur.await(); } System.out.print(name); state++; next.signal(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } } }这段代码的基本逻辑是:三个线程共用一把锁,每个线程负责打印一个字符,打印完唤醒下一个条件的线程。这里用while而不是if来检查状态,是因为线程被唤醒后要重新判断条件,防止假唤醒。这个细节如果能在注释里写清楚,面试官会高看一眼。
如果没有学过Condition,用synchronized + wait/notify也可以实现,思想一样,只是锁和唤醒对象不同。笔试时写那种会的不一定要求多优雅,但一定要保证能编译、能跑通。所以在平时的练习里,至少两个方案都要能默写出来。
4.3 编程题的答题规范和编码习惯
在线笔试系统一般只要求提交代码,不要求写注释,但代码质量仍然会通过面试复盘反映出来,有的公司还会把笔试代码打印出来让面试官看。所以从一开始就要养成几个习惯:
- 变量命名用语义化单词,不要一水儿的a、b、c
- 每个方法只做一件事,主方法里不要堆几百行
- 入参为空、长度为0的情况提前判断
- 循环里避免重复计算数组长度
- 递归注意退出条件和栈深度
这些习惯在校招阶段可能感觉不到收益,但到了真实项目里,代码可读性几乎等于可维护性。笔试题里最后一道编程题如果涉及链表或树的遍历,边界条件的处理占分比很高,空指针处理往往就是AC和部分AC的分水岭。
5. 常见问题与备考避坑清单
5.1 笔试作答顺序与时间分配策略
在线笔试通常由两部分组成:选择题和编程题。很多同学习惯从头做到尾,结果在选择题的计算题上耗掉大量时间,编程题最后没时间写。我踩过这个坑,所以我的建议是:
先花两分钟扫一遍全部选择题,把有明显把握的题先做掉,拿不准的标记跳过。然后直接进入编程题,写完一道再过下一道,最后再回头纠结选择题。原因很简单:编程题分值占比高且按测试用例给分,写一个暴力解还能拿部分分数,选择题蒙对的概率远低于这个。
具体到时间分配,如果总时长90分钟,20道选择题,建议控制在40分钟以内,剩下50分钟给编程题。每道编程题至少留10分钟用来调试和手动模拟用例。
5.2 最容易掉进去的知识点是这些
结合近几年的校招Java笔试题,有几个知识点出现的频率居高不下,但错误率也很高。
- 自动装箱与缓存:
Integer a = 127; Integer b = 127; a == b是true,但128就是false,因为Integer缓存范围默认是-128到127 - 位运算优先级:
&优先级低于==,写条件判断时不加括号很容易错 switch对String的支持:Java7之后支持,但底层是equals加hashCode- 数组与ArrayList的互相转换:
Arrays.asList()返回的是定长列表,不能add和remove ==与equals的区分:地址对比还是值对比,永远记住String的equals被重写过
这些点不难,但复习时容易被忽略。我当年就栽在Integer缓存那道题上,后来每场笔试前都会专门过一遍包装类的内容,再也没有错过。
5.3 笔试题型和面试题如何配合复习
笔试和面试的考点有重叠,但准备逻辑不完全一样。笔试答题需要速度和准确率,面试答题需要深度和表达。我的建议是准备笔试时,遇到一个知识点,就顺带把它的面试版也梳理一遍,形成“笔试会选题,面试会讲原理”的双层准备。
比如笔试考HashMap的负载因子,那就要能向面试官解释为什么要0.75;笔试考synchronized锁升级,面试就可能会问偏向锁和轻量级锁的区别、锁膨胀的触发条件。按照这个思路来复习,等于一套时间干了两份活,而且这种深度理解反过来也能提升笔试时对难题的直觉判断。
5.4 复盘比刷题更重要
很多人刷题就是做完对答案,然后继续下一题,这样做十套题和做一套题效果差不多。真正有效的复盘是记录每一道错题的知识点类型、错误原因和正确思路,然后每周回头重做一遍错题。
我备考后期有一个习惯:建一个“笔试错题本”文档,每套模拟题复盘完,把题目对应的考点按红黄绿三色标注,绿色代表熟练,黄色代表似懂非懂,红色代表完全不会。每周复习只攻红黄两色,绿色直接跳过。这个方法坚持一个月,明显感觉到选择题的正确率从六七成涨到九成左右。
编程题复盘则要更细,不是看一遍题解就算过,而是要把别人的思路默写出来,再尝试和暴力解做对比,分析复杂度差在哪里。这个过程刚开始很痛苦,但两周之后就会发现类似题型有了肌肉记忆。
最后再分享一个备考过程中的小感受:互联网公司笔试出题风格再变,考察内核始终是“基础是不是扎实、写代码是不是熟练”。比起买一堆资料到处找原题,不如把HashMap源码、线程池参数、排序算法手写题这一亩三分地耕透。尤其是爱奇艺这类有海量高并发业务场景的公司,一套卷子做下来,你其实能感受到它想找的是那种学完JavaSE还能动手写并发、懂一点JVM的人。备考时别只瞄着选择题的偏题怪题,多花时间把基础串成体系,编程题稳住,结果大概率不会差。