爱奇艺Java笔试真题复盘:从集合到并发,详解高频考点与手写代码思路

爱奇艺Java笔试真题复盘:从集合到并发,详解高频考点与手写代码思路 2019年秋招那会儿爱奇艺的Java方向笔试题B在求职圈子里流传度相当高。很多人拿它当模拟卷反复刷因为它不仅是视频平台对Java工程师基础能力的一次全面检验更代表了大厂笔试“考基础、考细节、考代码功力”的典型风格。我当时也认真复盘过这套题后来带新人准备校招还是会把它里面的考点单独拎出来讲一遍。这篇内容就是围绕这套题的完整复盘把高频考点、常见陷阱、手写代码的答题思路都过一遍。适合正在准备校招、跳槽的Java工程师也适合想系统梳理一遍Java核心知识的人。不管你现在处于哪个阶段把一套经典笔试题吃透比走马观花刷十套题有用得多。1. 先搞清楚出题人到底想考什么1.1 一套笔试题背后的命题逻辑爱奇艺这类视频平台后端技术栈以Java为主核心业务覆盖内容管理、播放调度、会员体系、弹幕评论、推荐系统等。这些业务有个共同特点高并发、高可用、大量IO操作。面试官坐在屏幕对面最想知道的是这个候选人有没有扎实的Java功底能不能在入职后快速接手业务代码。所以笔试几乎不会考具体的业务功能而是考“基础能力”。题量通常在60到90分钟题目的分布大致分三类概念选择题、代码阅读题、手写代码题。概念题考察记忆和理解代码阅读题考察对源码和执行流程的熟悉度手写代码题考察工程编码能力和边界处理意识。这套题里选择题和代码阅读题占比不低手写题集中在排序和数据结构上。有人会问2019年的题放到现在还有参考价值吗有。虽然Java版本从8升到了17甚至21但核心基础几乎没有变过。HashMap的扩容机制、JVM内存模型、synchronized和volatile的区别、快速排序怎么写这些依然是面试必考的内容。把一套经典题吃透相当于把Java面试最底层的知识体系过了一遍。1.2 知识点分布与分值倾向根据这套题的复盘出题范围其实相对固定。我整理成一张表方便复习时对照。模块典型考点考察意图Java基础String、包装类、运算符、异常语言功底是否扎实面向对象重载重写、多态、抽象类与接口设计能力与抽象思维集合框架HashMap、ArrayList、迭代器对常用数据结构的源码理解JVM内存区域、GC、类加载对运行时的理解深度并发编程volatile、synchronized、线程池高并发场景下的基本素养算法与数据结构冒泡、快排、二分查找、数组操作编码基本功分值和难度上Java基础类题目最多属于“必须拿分”的部分集合框架和JVM属于“拉开差距”的部分并发编程和手写算法属于“高分项”。如果目标是笔试通过优先保证前两块不出错后面两块能写多少是多少这样的策略最稳妥。2. Java基础与面向对象送分题里藏着不少坑2.1 字符串与包装类型String、Integer的经典陷阱Java基础类题目看似简单其实坑最多。String和Integer是高频主角。String面试题多数围绕“不可变性”和“常量池”展开。String是final修饰的底层字符数组也是final的所以任何修改操作都会生成新对象而不是改动原对象。这个特性带来了线程安全性和字符串常量池的复用机制。笔试题里常考的是这段代码的输出String s1 abc; String s2 abc; String s3 new String(abc); System.out.println(s1 s2); // true指向常量池同一个对象 System.out.println(s1 s3); // falses3指向堆上新对象 System.out.println(s1.equals(s3)); // true比较的是值s1和s2都从常量池取所以引用相同new String一定在堆上创建新对象即使常量池里已经有abc。如果笔试里再延伸问StringBuilder和StringBuffer的区别回答要点是StringBuffer线程安全、方法加了synchronizedStringBuilder线程不安全但性能更高单线程场景优先用StringBuilder。Integer的经典考法是缓存机制。直接看这段代码Integer a 100; Integer b 100; Integer c 200; Integer d 200; System.out.println(a b); // true System.out.println(c d); // false原因在于Integer在-128到127范围内有缓存装箱时直接返回缓存对象。超过这个范围就new新对象比较的是引用地址所以结果为false。这个知识点年年考我每次给新人讲都会多说一句比较包装类型是否相等一律用equals不要用除非你非常明确自己在比较引用。2.2 重载、重写与多态面向对象考点的主要形态面向对象三大特性是笔试必考但考法很细。最常见的是区分重载和重写重载发生在同一个类中方法名相同参数列表不同编译期就确定调用哪个方法属于编译期多态。重写发生在子类与父类之间方法签名相同运行期由实际对象类型决定调用哪个版本属于运行期多态。笔试题里常出的变体包括构造器能不能重写、静态方法能不能重写、重写方法的访问修饰符能不能更小。答案是构造器不能被重写静态方法可以被子类继承调用但不存在真正的重写因为它是编译期绑定重写方法的访问修饰符不能比父类更小否则直接编译报错。还有一道很典型的多态题结合了继承和初始化顺序class Parent { static { System.out.print(1 ); } { System.out.print(3 ); } public Parent() { System.out.print(4 ); } } class Child extends Parent { static { System.out.print(2 ); } { System.out.print(5 ); } public Child() { System.out.print(6 ); } } // new Child() 输出什么 // 1 2 3 4 5 6输出顺序是父类静态块、子类静态块、父类实例块、父类构造器、子类实例块、子类构造器。这个考点考察的是对类加载和对象创建过程的理解笔试中出现频率很高。抽象类与接口的区别也是高频题。Java 8之后接口允许默认方法和静态方法两者的界限开始模糊。我习惯这样记抽象类描述“是什么”接口描述“能干什么”。一个类只能继承一个抽象类但可以实现多个接口。如果题目问“什么场景选抽象类什么场景选接口”回答要点是抽象类适合抽取公共状态和公共方法接口适合定义能力契约。2.3 hashCode与equals的“连体”关系hashCode和equals是Java基础里最容易被低估的一对组合。如果你重写了equals却不重写hashCode后果会很隐蔽。举个例子。一个User类只重写equals比较id没有重写hashCode。往HashSet里放两个id相同的User对象时HashSet先调用hashCode计算存储位置两个对象hashCode不同被放到不同桶导致两个“相同”对象同时存在集合里这在逻辑上就错了。所以规则只有一条equals相等的两个对象hashCode必须相等hashCode相等equals不一定相等。HashMap查找元素时先用hashCode定位到桶再用equals比较链表中具体元素两个方法协作完成查找。这也是为什么有人在HashMap源码题里被问“为什么重写equals一定要重写hashCode”——因为不重写就破坏了这个协作契约。3. 集合框架HashMap几乎是必考核心3.1 HashMap的底层结构与扩容机制集合框架里如果只押一道题我肯定押HashMap。这套笔试题里HashMap相关的题目出现频率非常高考察点集中在底层结构、put流程、扩容机制。JDK 1.8之后HashMap的底层结构是“数组链表红黑树”。数组的每个槽位叫bucket存入元素时先对key的hashCode做扰动运算再通过(n - 1) hash计算槽位。hash冲突时用链表或红黑树存储。链表长度超过8且数组长度大于等于64时链表转红黑树把查找效率从O(n)降到O(logn)。负载因子默认0.75意思是元素个数达到数组长度的75%时触发扩容容量变为原来的两倍。为什么是0.75这是时间与空间的平衡。负载因子太小浪费空间太大冲突概率增加链表变长查询效率下降。0.75是官方经过多次测试的折中值。扩容时元素会重新计算槽位这也是HashMap在并发场景下可能丢数据的原因。所以笔试里如果问“HashMap线程安全吗”标准答案是线程不安全并发场景用ConcurrentHashMap。手写HashMap的put流程简述时建议按这个顺序说计算key的哈希值定位桶桶为空直接放入桶不为空判断首节点是否key相同相同则覆盖否则遍历链表或红黑树找到相同key则覆盖找不到则插入插入后判断size是否超过阈值超过则扩容。这个流程能一口气讲清楚说明你对源码是真的理解。3.2 ArrayList与LinkedList的选型问题ArrayList和LinkedList的对比题目表面问区别实际考的是数据结构复杂度。ArrayList底层是动态数组随机访问是O(1)中间位置插入删除是O(n)因为要移动元素。LinkedList底层是双向链表随机访问是O(n)头尾插入删除是O(1)。很多初学者会在“频繁插入删除”场景无脑选LinkedList但实际开发中这是一个误区。ArrayList的插入慢在于元素搬移但内存连续、CPU缓存友好LinkedList每个节点需要额外存储前后指针内存开销大且链表节点在内存中不一定连续遍历效率并不一定比ArrayList高。所以多数业务场景下ArrayList依然是首选除非你非常明确只在头部做大量插入。关于ArrayList还有一个细节值得注意默认容量是10扩容时newCapacity oldCapacity (oldCapacity 1)也就是1.5倍。知道这个公式在笔试里能直接得分。如果题目再深挖一点问“如何预估ArrayList容量避免频繁扩容”可以回答说在构造时传入初始大小new ArrayList(1000)减少扩容次数带来的性能损耗。3.3 集合遍历时如何安全删除元素有一道很经典的题目给一个ArrayList边遍历边删除元素问会不会抛异常为什么。ListString list new ArrayList(); list.add(a); list.add(b); list.add(c); for (String s : list) { if (b.equals(s)) { list.remove(s); } }运行后会抛ConcurrentModificationException。原因是迭代器内部维护一个modCount字段每次对集合结构性修改add、remove都会让modCount自增。迭代器在next时校验modCount是否发生变化变化了就抛异常。for-each本质是迭代器遍历所以在循环里直接remove会触发校验失败。安全删除有几种常见方式使用迭代器自身的iterator.remove()它会同步修改expectedModCount。倒序遍历用list.remove(index)删除避免索引错乱。使用Java 8的removeIf一行代码搞定。收集要删除的元素最后统一removeAll。removeIf是我在日常代码里用得最多的方式简洁且安全。笔试里如果能解释清楚modCount机制比只写对代码更能加分。3.4 Comparable、Comparator与lambda排序排序考点里笔试经常要求对对象数组或集合排序。Comparable是自然排序定义在类内部实现compareTo方法。Comparator是外部比较器可以定义多种排序规则不需要修改原类。从Java 8开始Comparator配合lambda表达式让写法变得非常简洁。比如对一个User列表按年龄排序ListUser users getUsers(); users.sort(Comparator.comparingInt(User::getAge));按年龄降序users.sort(Comparator.comparingInt(User::getAge).reversed());先按年龄降序再按姓名升序users.sort(Comparator.comparingInt(User::getAge) .reversed() .thenComparing(User::getName));这类代码在笔试里出现时考察点不单纯是API记忆而是你是否理解排序的稳定性和链式调用逻辑。我的建议是Comparable和Comparator的区别要说清楚“自然排序”和“定制排序”两个词lambda语法要能现场手写。如果笔试要求不用lambda用匿名内部类也要能写出来。4. JVM与并发拉开差距的硬核题4.1 内存区域与OOM看到报错要能定位JVM相关的题这套里考过内存区域划分和OutOfMemoryError的场景判断。如果你在简历上写“熟悉JVM”这几乎是必问项。Java运行时数据区分为线程私有的虚拟机栈、本地方法栈、程序计数器以及线程共享的堆、方法区。JDK 8以后方法区被元空间取代元空间使用本地内存不再受堆大小限制。堆内存是对象分配的主要区域又分为新生代和老年代新生代里再有Eden区和两个Survivor区。OutOfMemoryError是笔试喜欢结合代码问的题目。new大量对象导致堆内存不足报java.lang.OutOfMemoryError: Java heap space创建线程过多导致无法创建本地线程报unable to create new native threadJava 8之前还有永久代溢出现在的元空间默认也有上限。排查思路一般是先看堆栈判断是堆溢出还是栈溢出再决定是调大内存还是优化代码。来看一个简单的JVM参数配置示例java -Xms512m -Xmx2g -XX:UseG1GC -jar app.jar-Xms是初始堆大小-Xmx是最大堆大小-XX:UseG1GC指定使用G1垃圾回收器。这里有两个容易踩的坑第一-Xmx必须不小于-Xms否则JVM启动直接报错第二生产环境建议把两者设为相等避免运行期动态扩容引入性能抖动。还有一个常见报错是“源发行版17需要目标发行版17”这属于编译版本配置问题不是JVM运行时问题但面试时经常被混在一起问需要区分开。4.2 垃圾回收与常用收集器GC相关题目主要考三点怎么判断对象可以回收、有哪些收集算法、常见收集器的区别。判定对象是否为垃圾主流方案是可达性分析。从GC Roots出发沿着引用链遍历不可达的对象被标记为可回收。GC Roots包括虚拟机栈中引用的对象、静态变量引用的对象、本地方法栈引用的对象等。引用计数法因为循环引用问题在主流JVM中已经不用了。收集算法方面新生代用复制算法因为新生代对象大多朝生夕死复制成本低老年代用标记-清除或标记-整理。CMS是并发标记清除收集器以低停顿为目标缺点是会产生内存碎片G1是分区的垃圾回收器把堆分成多个Region能够做到可预测的停顿时间。现在的JDK 17里G1是默认收集器ZGC的停顿时间更是低到了毫秒级。笔试里如果问到“什么场景触发Full GC”标准答案是老年代空间不足、元空间不足、调用System.gc等。排查Full GC的思路是观察GC日志看每次Full GC回收了多少内存。如果回收很少但频繁发生大概率是内存泄漏或对象被长期持有。这类题需要结合实际场景回答能举出“大对象直接进入老年代”、“内存泄漏导致频繁Full GC”这些例子会更专业。4.3 volatile与synchronized并发编程的一对关键概念并发编程的题几乎是秋招笔试的标配。volatile和synchronized的区别是一道必背题但我更建议你理解它背后的“看得到”和“碰得到”。volatile解决的是可见性和有序性问题。它保证一个线程修改了变量其他线程能立刻看到变化且禁止指令重排序。但它不保证原子性。经典例子volatile int count 0; // 线程A count; // 线程B count;count看似一步实际上是“读取-计算-写入”三步volatile只能保证每次读取都是最新值但读取和写入之间可能被其他线程插队所以最终结果可能小于2。需要原子性时改用AtomicInteger或加锁。synchronized是重量级锁但经过锁升级优化后并没有大家想象的那么重。synchronized可以修饰方法也可以修饰代码块作用是保证同一时刻只有一个线程执行临界区代码同时具备可见性。它的底层依赖对象监视器monitorJDK 1.6之后引入了偏向锁、轻量级锁、重量级锁的升级过程。这张对比表可以直接背对比项volatilesynchronized可见性保证保证原子性不保证保证有序性禁止指令重排保证能否修饰方法只能修饰变量可以性能轻量相对较重有优化4.4 线程池参数与拒绝策略线程池是并发题目的另一个重点。Java提供的ThreadPoolExecutor有七个构造参数笔试高频考的是核心线程数、最大线程数、阻塞队列和拒绝策略。参数含义corePoolSize核心线程数即使空闲也会保留。maximumPoolSize最大线程数。workQueue任务队列线程都在忙时新任务进入队列等待。keepAliveTime非核心线程空闲存活时间。threadFactory线程工厂。handler拒绝策略。任务提交流程是先判断当前线程数是否小于核心线程数小于则新创建线程否则尝试放入队列队列满了再判断是否小于最大线程数小于则创建新线程还是不满足走拒绝策略。四种拒绝策略要记住AbortPolicy默认直接抛RejectedExecutionException。CallerRunsPolicy由提交任务的线程自己执行任务。DiscardPolicy直接丢弃任务不抛异常。DiscardOldestPolicy丢弃队列中最旧的任务然后重新提交当前任务。线程数怎么设置分场景。CPU密集型任务建议设置为CPU核数 1IO密集型任务线程可以多一些常用公式是CPU核数 * (1 平均等待时间 / 平均计算时间)。笔试里能说出来这个公式说明你有实际思考。另外一句老生常谈阿里规约里建议不要用Executors的快捷方法创建线程池因为FixedThreadPool和SingleThreadPool的队列是Integer.MAX_VALUE可能堆积大量请求导致OOMCachedThreadPool最大线程数是MAX_VALUE可能创建大量线程导致资源耗尽。建议手动new ThreadPoolExecutor把参数掌握在自己手里。5. 手写算法与代码规范这类题怎么拿分5.1 高频排序冒泡排序与快速排序手写排序是笔试保留项目。这套题里冒泡和快排都出现过。别觉得简单越基础的题越能看出代码习惯。先看冒泡排序public static void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; } // 外层控制轮数 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]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } // 如果一轮下来没发生交换说明已经有序提前退出 if (!swapped) { break; } } }冒泡的优化点在于用一个swapped标志位判断本轮是否发生交换如果没交换说明数组已经有序提前结束。时间复杂度最好O(n)最坏O(n²)平均O(n²)。笔试时写冒泡最容易被忽略的就是内层循环的- i少了它虽然结果可能对但多做了很多无效比较。快速排序考得更多因为它涉及递归和分区思想public static 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 static int partition(int[] arr, int left, int right) { int pivot arr[left]; int i left, j right; while (i j) { while (i j arr[j] pivot) { j--; } arr[i] arr[j]; while (i j arr[i] pivot) { i; } arr[j] arr[i]; } arr[i] pivot; return i; }这里用的是“挖坑法”把基准值先存起来留一个坑然后从右往左找比基准小的填坑从左往右找比基准大的填坑最后把基准放回i的位置。快排的平均时间复杂度是O(nlogn)最坏O(n²)比如数组本身有序且每次选第一个元素作基准时。笔试手写时只要分区逻辑清晰、递归边界正确就能拿到大部分分数。排序算法还有个知识点容易被忽略稳定性。冒泡、插入排序是稳定的快排、堆排序是不稳定的。如果题目问“什么场景需要用稳定排序”可以回答按多个字段依次排序时稳定排序能保留前一次排序的相对顺序比如Excel里先按日期排序再按优先级排序。5.2 数组越界与边界条件一场隐藏的“扣分战”数组越界异常ArrayIndexOutOfBoundsException是笔试代码题最常见的报错。很多候选人思路对了结果因为边界条件处理不当运行直接崩了。典型场景是二分查找。先看一个容易写错的版本// 错误写法可能溢出 int mid (left right) / 2;当left和right都很大时left right可能溢出结果mid变成负数再访问arr[mid]就数组越界。正确写法int mid left (right - left) / 2;二分查找的循环条件是left right移动边界时用mid - 1和mid 1避免死循环。笔试里写代码边界条件是第一扣分点第二是空数组、单元素数组、重复元素数组没有测试意识。我一般建议大家写完代码后立刻在脑子里跑三个用例空数组、长度1的数组、最大长度数组。还有一个常见的数组比较问题不能用arr1 arr2比较两个数组内容是否相等因为比较的是引用地址。比较内容要用Arrays.equals(arr1, arr2)。这个知识点虽然基础但在笔试题里经常被用来设置陷阱。5.3 从笔试题到面试如何在一道题里展示工程素养笔试不是只改判对错很多公司会有人工复查。同样的代码评分差距可能来自细节。命名习惯很重要。临时变量用i、j可以但算法核心变量最好能表达语义比如pivot、left、right比a、b、c好得多。防御式编程也加分入口处判空、判断长度小于2直接返回这些看起来多余实际是工程素养的体现。写完之后标注时间复杂度和空间复杂度也是很多高分答卷的共性。代码格式上缩进统一、花括号换行风格一致、没有多余空行这类细节会被阅卷人看在眼里。笔试环境下时间紧张不一定能写得完美但至少保持清晰易读。我见过很多候选人思路完全正确但代码乱得没法看白白丢分。另外一个容易被忽视的点是如果题目要求输出结果而不是写代码那么一定要把输入输出格式看清楚。曾经有人快排写对了结果main函数读取数据的方式不对整个程序跑不起来这种失误是最可惜的。6. 备考与实战我复盘这套题的一些心得6.1 复习优先级与时间分配建议如果你准备时间有限按这个顺序复习性价比最高Java基础与面向对象包括String、Integer、hashCode/equals、抽象类与接口这部分拿分最稳2到3天可以过完。集合框架重点HashMap、ArrayList、迭代器配合源码阅读3到4天。JVM基础内存区域、垃圾回收、类加载这部分广度优先4到5天。并发编程volatile、synchronized、线程池2到3天。手写算法每天固定练3道题优先排序、二分、链表反转。这个顺序的逻辑是先易后难先把基础分拿到手再攻硬骨头。这套题整体难度中上如果前面五个模块都扎实通过笔试的概率很大。刷题的时候不要只背答案要把每道题的考点扩展成一篇笔记。比如遇到HashMap的题可以顺带把ConcurrentHashMap、HashTable、LinkedHashMap的对比都整理一遍。知识是网状的单点记忆很容易忘连成网才牢固。6.2 这套题之外你还需要准备什么笔试只是第一关通过之后还有面试。笔试里考到的知识点面试会问得更深。比如HashMap会延伸到ConcurrentHashMap的实现原理JVM会延伸到线上OOM怎么排查线程池会问你实际项目里怎么设置参数。除此之外数据库、Spring全家桶、Redis、消息队列也是面试高频区。2019年那会儿Spring Boot和微服务已经开始流行现在更是人人必谈。如果你只刷Java基础题过了笔试也容易挂在技术面。我建议翻一翻最新的JDK版本特性。这几年JDK 8的历史地位还在但17和21已经普及新版本里的record、switch表达式、虚拟线程等特性正逐渐成为面试话题。学有余力时把JDK 8到21的关键特性过一遍会有惊喜。这里有个经验在简历上写“熟悉Java 17新特性”面试官很可能会顺着问虚拟线程和ZGC准备充分的话是很好的加分项。6.3 常见错误清单考前看一眼能少踩一半坑我把平时带人刷题时遇到的常见错误整理成一张速查表考前十分钟翻一遍很有用。错误类型具体表现正确做法包装类型用Integer超127比较结果出错用equals遍历时直接remove抛ConcurrentModificationException用iterator.remove或removeIfHashMap并发使用多线程put丢数据用ConcurrentHashMapvolatile用于计数count结果不准用AtomicInteger或加锁二分查找mid溢出leftright溢出导致越界用left (right - left) / 2线程池用Executors队列或线程数无上限手动new ThreadPoolExecutor快排递归无边界栈溢出注意left right时return数组比较用比较的是引用Arrays.equals(arr1, arr2)这张表里的坑我在面试别人时见过无数次。别小看这些细节笔试的分数往往就是这些细节堆积出来的。我个人刷题的感觉是这套题放到今天依然没有过时。Java面试的底层逻辑始终没变——基础要牢原理要懂代码要能写。当时我把每道题的知识点扩展成笔记后来面试时遇到类似问题都能从容应对。如果你正在准备这类考试不妨把这道题当成一面镜子看看自己在哪块最薄弱集中火力补上。最后再分享一个小方法刷完题别急着看答案先独立把思路写下来哪怕写错了也比直接背答案有用得多。这个习惯让我受益至今写代码、排查问题、甚至带新人都靠它。笔试没有什么捷径把一道经典题吃透比走马观花刷十套题更有价值。