1. 从这套题看爱奇艺Java岗的考察偏好
先来聊一个可能让很多人意外的事实:爱奇艺这套2019秋招Java笔试题(B),放到现在依然有很高的参考价值。那年秋招我还在一家小厂做Java后端,整套做下来最大的感受是——它的考点分布和大多数互联网公司的Java笔试题高度重合,但有几个题目明显是在“筛选”,不是单纯考你会不会,而是考你为什么。
我当时刷完这套题,对照牛客网上的讨论区和几份面经,整理出的考点分布大概是这样的:
| 考察模块 | 题量占比(估算) | 典型考点 |
|---|---|---|
| Java语法与面向对象 | 25%左右 | 继承、多态、String、包装类 |
| Java集合框架 | 20%左右 | ArrayList/LinkedList、HashMap、HashSet |
| 并发编程 | 15%左右 | synchronized、volatile、线程池 |
| JVM与内存模型 | 15%左右 | 内存区域、GC、类加载 |
| 算法与数据结构 | 15%左右 | 排序、链表、二叉树 |
| 网络与操作系统 | 10%左右 | TCP、HTTP、进程线程 |
注意,这只是我根据回忆和面经做的粗略统计,不是原卷的标准答案比例,但方向是准的。这套题的核心价值不在于题目本身,而在于它代表了一类“基础扎实型”笔试题的典型出题思路。如果你准备的是BAT、TMD这类公司的Java校招,这套题的复习价值会非常大。
为什么这么说?因为2019年秋招恰好是“Java八股文”面试文化形成的关键年份。那之前的笔试题更偏向“会不会用”,那之后越来越偏向“源码看过没有、原理懂不懂”。这套B卷刚好卡在中间——既有一些纯记忆型的基础题,又已经开始出现需要看源码才能答对的题目。做这套题最大的收获,是能帮你校准自己的Java基础到底处于哪个水位线上。
2. Java基础与面向对象:经典题目里的易错点
这套B卷里的Java基础题,大部分并不难,但有几个细节题值得好好掰扯一下。这种题不是考你背不背得出来,而是考你在实际写代码时有没有踩过对应的坑。
2.1 继承与多态:重载和重写的判定顺序
有一道很典型的题,给出一段父子类代码,问调用某个方法时输出什么。这种题我在实际面试中见过不下十次,但每次扔给身边的同事做,还是有人会答错。
核心就三条规则:
- 编译看左边,运行看右边——变量声明类型决定编译期能调用哪些方法,实际执行的是堆上对象类型的方法。
- 重载是静态绑定的,编译期根据参数的静态类型决定调用哪个重载方法。
- 重写是动态绑定的,运行期根据对象的实际类型决定调用哪个方法。
举个例子,有一个经典的坑:
class Animal { public void eat() { System.out.println("Animal eat"); } } class Dog extends Animal { public void eat() { System.out.println("Dog eat"); } public void bark() { System.out.println("Dog bark"); } } Animal a = new Dog(); a.eat(); // 输出 Dog eat,动态绑定 // a.bark(); // 编译报错,编译看左边的Animal类型当时群里有个同学做这道题,选了“Animal eat”,理由是“变量是Animal类型,所以调用Animal的方法”。这就是典型的把编译期类型检查和运行期动态绑定搞混了。方法重写是动态绑定的,不管引用类型是什么,实际执行的是堆上那个对象的方法。
2.2 String相关的陷阱:不可变性与常量池
String的考察点也几乎每年都有。爱奇艺这套题里有一道和String比较相关的题,问的是s1 == s2的结果。这类题的核心是字符串常量池和不可变性。
最经典的几行代码:
String s1 = "hello"; String s2 = "hello"; System.out.println(s1 == s2); // true,都在常量池,同一对象 String s3 = new String("hello"); System.out.println(s1 == s3); // false,new出来的在堆上 String s4 = s3.intern(); System.out.println(s1 == s4); // true,intern返回常量池中的引用我见过有人把第一行的结果答成false,原因是“两个变量指向两个不同的对象”。这个理解不对——直接双引号声明的字符串字面量,会先到常量池里找,如果存在就直接复用,所以s1和s2实际指向同一个常量池对象。
还有一个容易忽视的点,字符串拼接:
String a = "hello"; String b = a + " world";这种写法在编译期无法确定值(因为a是变量),所以实际是通过StringBuilder拼接后生成新对象。如果换成final String a = "hello",那编译期就能确定a的值,b会直接指向常量池中“hello world”。这个差异在笔试里容易被考到,实际开发中则提示我们:频繁拼接字符串一定要用StringBuilder,否则会创建大量中间对象。
2.3 包装类与缓存机制:Integer缓存的范围
自动装箱、拆箱也是高频考点。爱奇艺这套题里应该有一道和Integer相关的比较题,问Integer a = 127; Integer b = 127; a == b的结果。
答案是true,因为Integer默认缓存了-128 ~ 127之间的值,valueOf方法会直接返回缓存对象。但如果是Integer a = 128; Integer b = 128;,结果就是false,因为超出了缓存范围,会new出两个新对象。
这个机制我在实际开发中踩过坑——当时有一段代码用Integer做 Map 的 key,取出来比较的时候用==,测试环境的数据都在127以内,一切正常;线上数据超过127,就出现了完全无法复现的bug。排查了半天,最后发现是==比较的锅。教训就是:包装类型比较一律用equals,不要用==。这个习惯如果能在笔试前就养成,考试时这种题根本不需要思考。
这些基础题在我看来是送分题,但也是最容易因为“一看就会、一写就错”而丢分的题。建议做这类题时,别急着选答案,先在心里把运行过程完整推演一遍,尤其是涉及继承、重载、字符串拼接时。
3. 集合框架源码题:从使用到原理的跨越式考察
爱奇艺这套B卷的集合题,难度明显比基础题上了一个台阶。如果你只看过《Java编程思想》里集合框架的用法,但没有翻过JDK源码,这部分会很吃力。
3.1 ArrayList和LinkedList:不只是“数组vs链表”
有一道常见的题是问ArrayList和LinkedList的区别。大部分人能答上来“ArrayList底层是数组,LinkedList底层是双向链表;查询ArrayList快,插入删除LinkedList快”。但在笔试题里,这个答案只能拿一半分,因为题目会继续深挖。
比如:
ArrayList的初始容量是多少?默认是10,但注意是懒加载——第一次add时才真正初始化数组为容量10。ArrayList扩容是怎么做的?grow方法里新容量是oldCapacity + (oldCapacity >> 1),也就是1.5倍,然后通过Arrays.copyOf复制旧数组。LinkedList为什么插入删除快?因为它是双向链表,插入删除只需要改变指针,但如果指定了索引位置,LinkedList需要从头或尾遍历到该位置,时间复杂度是O(n)。所以“LinkedList插入删除快”这句话是有条件的,插入在头部或尾部确实快,随机位置插入反而可能比ArrayList慢。
我在做这道题时还想到一个问题:如果频繁在列表头部插入数据,应该选什么?ArrayList在头部插入需要移动所有元素,LinkedList在头部插入只需要改指针。但ArrayDeque在这个场景下比LinkedList更合适,因为它的底层是循环数组,头部插入的时间复杂度是O(1),且省去了链表节点的额外内存开销。这个知识点在笔试里如果主动写出来,会是一个不错的加分项。
3.2 HashMap并发问题与ConcurrentHashMap的演进
HashMap相关题目在Java笔试里几乎占了半壁江山。爱奇艺这套题里,HashMap的考察点涵盖了数据结构、put流程、扩容机制、并发安全性。
JDK 8之后的HashMap结构是数组+链表+红黑树。当链表长度超过8且数组长度大于等于64时,链表会转成红黑树;当红黑树节点数小于等于6时,会退化为链表。为什么转树阈值是8?因为源码注释里有一个泊松分布的分析,在负载因子0.75、随机哈希函数的前提下,链表长度达到8的概率已经非常低(约千万分之六)。这是时间和空间的权衡,不是随便拍脑袋定的数字。
HashMap的put流程,笔试时最好能画出来:
- 计算
(n - 1) & hash,确定桶的位置 - 如果桶为空,直接new一个Node放进去
- 如果桶不为空,遍历链表或红黑树
- 如果找到相同key,覆盖value并返回旧值
- 如果没找到,插入到链表尾部或红黑树
- 插入后检查
size > threshold,如果是,触发扩容
而并发问题方面,有一个经典考题:HashMap在多线程环境下put为什么会丢数据?因为多线程同时触发扩容时,多个线程可能同时在处理同一批链表节点,导致节点覆盖、丢失。JDK 7里还会因为头插法形成环形链表,导致get时死循环。JDK 8改成尾插法解决了循环问题,但数据丢失问题依然存在。
所以笔试题里只要问到并发场景下的Map,答案必然是ConcurrentHashMap。但更要深入一层的是:JDK 8的ConcurrentHashMap抛弃了JDK 7的分段锁,改用CAS + synchronized锁桶头节点的方式。锁的粒度从段级细化到桶级,并发度大幅提升。这里的CAS是用来处理桶为空时的初始化,synchronized则用来处理桶不为空时的读写冲突。它依然不能保证绝对一致性强,但能保证单个操作的原子性和内存可见性。
我当时在准备这部分时,是直接把源码里的putVal、resize、treeifyBin这几个方法从头到尾读了一遍,然后再回来做笔试题,基本就都是降维打击了。我的建议是,集合框架这部分一定要看源码,不能只看面经总结。
4. 并发编程与JVM:最能拉开差距的得分点
这套题里最拉分的部分,我体感是并发和JVM。这里的基础版本大概就是“synchronized和ReentrantLock的区别”“JVM内存分为哪几块”这种,但高分版本会深入到底层原理和实际排查经验。
4.1 synchronized的锁升级:无锁→偏向锁→轻量级锁→重量级锁
synchronized的锁升级过程,几乎每次面试都会问到。我第一次在笔试里见到这个考点时,坦白讲,当时是我学习Java以来最崩溃的瞬间之一。为什么我连“synchronized能保证原子性和可见性”都背得滚瓜烂熟了,还要知道锁怎么升级?
后来我才理解:锁升级是理解Java并发性能的关键。synchronized在JDK 6之前是重量级锁,每次加锁都需要操作系统mutex,性能极差。JDK 6引入偏向锁和轻量级锁之后,性能大幅提升。现在你写synchronized,虚拟机会根据竞争情况自动决定锁的状态:
- 无锁状态:没有线程访问同步块。
- 偏向锁:第一个线程访问时,在对象头Mark Word里记录线程ID,之后这个线程再次进入时不需要任何CAS操作。适合“同一个线程反复进入”的场景。
- 轻量级锁:有其他线程竞争时,偏向锁撤销,升级为轻量级锁。线程通过CAS在栈帧中创建锁记录,自旋等待持有锁的线程释放。
- 重量级锁:自旋超过阈值(默认10次或自适应)或者等待线程数过多,升级为重量级锁,此时线程进入阻塞队列,由操作系统调度。
笔试经验是:只要题干里出现“单线程反复进入同步块”,优先考虑偏向锁;“少量线程交替执行”,优先考虑轻量级锁;“大量线程竞争激烈”,锁定重量级锁。
4.2 volatile的可见性与禁止重排序
volatile也是必考题。很多人能答上来“保证可见性、不保证原子性”,但问到“为什么不保证原子性”“底层怎么保证可见性”就卡住了。
- 可见性:volatile变量在写操作时会强制将工作内存中的值刷新到主内存,并让其他线程的缓存行失效(通过缓存一致性协议,如MESI)。读操作时直接从主内存读取最新值。
- 禁止重排序:volatile通过内存屏障实现。在写volatile变量之后插入StoreStore屏障,在读volatile变量之前插入LoadLoad屏障,防止编译器和CPU重排序。
- 不保证原子性:比如
count++实际上有“读取count→计算count+1→写回count”三步,volatile只能保证每一步的可见性,不能保证这三步作为一个整体不被其他线程打断。
有一个常见的坑是:很多人以为volatile修饰的变量做自增是线程安全的。不是的。这就是一道经典的笔试题变体。如果自增操作需要原子性,应该用AtomicInteger的compareAndSet或incrementAndGet,或者用synchronized包裹整个操作。
4.3 JVM内存区域与OOM定位
JVM内存区域的划分,基本是必考的。堆、虚拟机栈、本地方法栈、方法区(元空间)、程序计数器,每个区域的作用、是否线程私有、可能出现的异常,都要烂熟于心。
爱奇艺这套题里,JVM的考察方式更多是给一个场景,让你判断会抛出什么异常。比如:
- 堆溢出:
java.lang.OutOfMemoryError: Java heap space - 栈溢出:
java.lang.StackOverflowError(无限递归) - 元空间溢出:
java.lang.OutOfMemoryError: Metaspace(大量动态生成类) - 直接内存溢出:
java.lang.OutOfMemoryError: Direct buffer memory(NIO使用不合理)
实际开发中,定位OOM问题的思路基本上是:
- 在JVM启动参数里加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump,让虚拟机在OOM时自动导出堆转储文件。 - 用
jmap -dump:format=b,file=heap.hprof <pid>手动导出堆转储。 - 用MAT或VisualVM分析,重点看支配树——哪个对象占用了最大内存,它的GC Roots引用链是谁。
- 结合业务代码判断是内存泄漏(对象无法回收)还是内存溢出(对象确实都活着,但太多了)。
这些内容笔试不一定考得特别深,但面试官经常会顺着笔试题追问下去,所以备考时不能只背题,要把题背后的排查链路也串起来。我在后来的工作里,有一半以上的线上问题排查时间都花在JVM调优和OOM分析上,这些功夫在笔试阶段就开始打基础,确实不亏。
4.4 类加载机制与双亲委派
类加载这块也是高频考点,尤其是双亲委派模型。原理不复杂:当一个类加载器收到类加载请求时,它不会自己先去加载,而是先把这个请求委派给父类加载器,每一层都这么做,最终传到启动类加载器(Bootstrap ClassLoader),只有父类加载器无法完成加载时,子类加载器才会自己尝试加载。
这里有一个笔试时经常出的细节题:双亲委派模型能保证什么?答案是保证Java核心类库的类型安全。比如java.lang.String,无论哪个类加载器加载它,最终都会委托给启动类加载器,保证所有类加载器加载到的String都是同一个类,避免核心类被随意覆盖。
还有一个考察点是打破双亲委派的场景:Tomcat的WebAppClassLoader、JDBC的DriverManager、SPI机制下的ServiceLoader等。Tomcat为什么打破?因为每个Web应用应该拥有独立的类库,互不干扰;JDBC为什么打破?因为DriverManager在启动类加载器加载,而具体的数据库驱动在应用类加载器加载,需要逆向委派。
5. 算法与手写代码题:排序、链表和设计模式
爱奇艺这套B卷的算法题不算特别难,但有几道很有代表性。网上的解题方案很多,这里只聊两个重点:排序的边界条件和手写代码的规范程度。
5.1 冒泡排序和快速排序的现场实现
算法题里出现排序的频率极高,2019年的笔试题尤其爱考冒泡和快排。但注意,笔试考的不是你能默写出来,而是你能不能写出“一遍过”的代码。
以快速排序为例,最标准的写法是:
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 pivot = arr[left]; int i = left, j = right; while (i < j) { // 注意:先从右往左找小于pivot的 while (i < j && arr[j] >= pivot) { j--; } while (i < j && arr[i] <= pivot) { i++; } if (i < j) { swap(arr, i, j); } } swap(arr, left, i); return i; }有几个容易出错的点,实战中每次都有人栽在上面:
- 递归结束条件必须是
left >= right,不是left == right。当数组长度为2时,pivot可能等于left,递归quickSort(arr, left, pivot - 1)时left会大于right,少了等号判断就是无限递归。 - 先从右往左找,再从左往右找,顺序不能乱。因为基准选的是最左边的元素,如果先从左往右找,最后交换
arr[left]和arr[i]时,可能把一个大于pivot的值换到最左边,导致排序错误。 - 边界条件里
>=和<=不能丢,否则遇到重复元素时可能左右指针无法正常移动,陷入死循环。
冒泡排序的考点则在优化上:如果某一轮比较中没有发生任何交换,说明数组已经有序,可以提前退出。这个优化在笔试中写出来,妥妥的加分项,代码也不复杂:
public void bubbleSort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { boolean swapped = false; for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1); swapped = true; } } if (!swapped) { break; } } }5.2 手写单例模式:饿汉式、懒汉式和双重检查锁
设计模式在笔试题里常以手写代码的形式出现,最常考的就是单例。爱奇艺这套题里应该有一道“写一个线程安全的单例”题。
我当时在试卷上写的是双重检查锁:
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }这里有个关键点,instance必须用volatile修饰。为什么?因为new Singleton()在底层有三步:分配内存、初始化对象、将引用指向内存。如果不用volatile,第二步和第三步可能被重排序——先指向内存(此时对象还没初始化完),另一个线程拿到引用后发现instance != null,直接返回一个未初始化完成的对象,程序就出诡异问题了。volatile通过内存屏障禁止了这个重排序。
但笔试时如果只写双重检查锁,可能还不够体现水平。更优雅的写法是静态内部类:
public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }静态内部类的方式利用了类加载机制——Holder只有在getInstance()第一次被调用时才会被加载,加载时初始化INSTANCE,由JVM保证线程安全。这是既懒加载又线程安全的最简洁方案。笔试时如果时间充裕,可以把双重检查锁的代码写出来,再补充一句“更推荐静态内部类写法”,面试官印象分会拉满。
5.3 链表反转与二叉树遍历
算法题里链表和二叉树也是常客。链表反转要求写出迭代和递归两种实现,这个考的是代码熟练度,没有太多技巧含量,但容易在细节上出错,比如:
- 反转时要注意保存下一个节点的引用,否则指针断了找不到后续节点。
- 递归实现时递归的结束条件是
head == null || head.next == null。
二叉树这块,爱奇艺这套B卷的题目应该不会太难,层序遍历、前中后序遍历、求深度这类基础题要手熟。准备笔试时不需要刷特别难的题,但高频基础题必须做到闭着眼睛能写出来。
6. 复盘这套题之后:我对备考与做题顺序的几点经验
最后聊聊备考这套题时的整体复盘和一些实操经验,以下内容偏个人向,但很多坑是共通的。
6.1 先做真题,再看面经,最后补源码
很多人备考时习惯先刷一遍面经,觉得面经覆盖全面,然后才去做真题。但我的经验是反过来的:先限时做真题,把不会的题目标记出来,再回头去查对应的知识点和面经,最后深入研究JDK源码。这样做的好处是,你很清楚自己哪里薄弱,复习时有针对性,不会做大量无效的“扫盲式学习”。
做真题时尽量模拟真实考试环境:75分钟闭卷,不看答案,调好闹钟。第一遍做的时候分数低很正常,重要的是把每道错题背后的知识点吃透,而不是背答案。
6.2 检查代码题时,重点检查边界条件和空指针
笔试时代码题最容易丢分的地方不是算法逻辑,而是边界条件。数组为空时怎么办?链表只有一个节点时怎么办?字符串为null时调用length()会不会NPE?
我见过很多人算法主体写得完全正确,但没判空,直接导致用例失败。笔试的评测机通常会特别构造边界用例,空数组、单元素数组、超过int范围的值,这些都必须考虑到。一个简单的习惯是:写完代码后,在脑子里手动跑一遍arr.length == 0、arr.length == 1、n == 1的情况。
6.3 关于“背八股文”的真实看法
网上关于“Java八股文”的争论一直很大,但经历过校招之后,我的态度是:八股文要背,但不能只背。背是为了快速建立知识框架,确保面试时不会说不出话;但笔试和面试真正拉开差距的,是你能不能把八股文背后的原理讲清楚。
比如你可以背“HashMap线程不安全”,但如果面试官追问“为什么不安全”“JDK 7和JDK 8有什么区别”“ConcurrentHashMap为什么线程安全”,你还得能答上来。八股文是树干,源码和实战经验是枝叶,缺一不可。
6.4 这套题里少了什么
再说几句实话。爱奇艺这套2019年的Java笔试题(B),覆盖了Java基础、集合、并发、JVM、算法这些核心模块,但整体难度属于中等偏上,没有深入到Spring、微服务、分布式这些框架层面的知识。如果你拿这套题作为唯一的备考资料,可能会导致知识面明显偏窄。
我当时做这套题时,考完后立刻补了Spring IOC/AOP、Spring Boot自动配置、MySQL索引与事务隔离级别、Redis数据结构与持久化这几块。笔试只是第一次筛选,后面的面试会考得更广、更深。这套B卷适合作为校招Java基础的摸底和练手资料,但备考体系还需要用更多真题和项目经验来填充。
最后再分享一个小细节:做题时如果遇到读不懂题意的题目,先跳过,别硬耗。我当时在爱奇艺这套B卷上有一道关于JVM调优参数的题目,题干描述得模棱两可,我花了将近十分钟才弄明白它到底想问什么,结果挤占了后面算法题的时间。考试时遇到理解成本高的题目,先做个标记跳过,把有把握的分数拿稳了再回头啃。这个策略帮我保住了一道快速排序的大题,后来复盘时觉得当时如果死磕那道JVM题,整场笔试的节奏就全乱了。