2020年对Java程序员来说,最直观的变化就是面试几乎全部转到了线上。笔试放在牛客网,视频面试一场接一场,我前前后后投了阿里、腾讯、字节跳动、美团、拼多多几家Java后端岗,把大厂笔试和面试的流程完整走了一遍。这篇文章就是我的笔经面经复盘,覆盖Java基础、集合、JVM、并发、Spring、分布式和项目场景题,也把面试现场踩过的坑一并整理出来,给正在准备Java大厂面试的人做个参考。
1. 2020年Java大厂面试到底在考什么
1.1 线上面试带来的流程变化
2020年的招聘节奏和以往明显不一样。因为客观环境影响,大厂校招和社招基本都转成了全程线上进行,流程一般是:简历筛选、在线笔试、两到四轮技术面、主管面,最后是HR面。笔试从线下纸质答题换成了牛客网、赛码网这类在线平台,摄像头监考,有的公司还会要求开屏幕共享,现场写代码。
线上面试对候选人来说有利有弊。好处是省去了异地奔波的成本,面试场次可以安排得比较密集,我最多的时候一天面了三家。坏处是网络和设备不可控,尤其屏幕共享手写代码时,如果IDE突然报错或者网络卡顿,整个节奏都会被打乱。后来我学乖了,每次线上面试前都会提前检查本地JDK版本、Maven仓库、IDE编译设置,确保打开就能写代码。有一个小技巧:关闭IDE里所有不相关的弹窗和插件更新提示,避免写代码时突然弹出影响观感。
1.2 我理解的考察链路
大厂Java面试的考察链路其实高度统一:Java基础、集合容器、JVM、并发、Spring框架、分布式、项目场景,基本按照这个顺序从浅入深。第一轮技术面通常在Java基础、集合、并发和JVM上展开,第二轮开始聊项目、分布式、系统设计,主管面更关注项目深挖、解决问题能力和业务理解。
这也解释了为什么"八股文"在大厂面试里地位这么高。很多人反感背八股文,但如果把这些知识点理解透了,它们其实就是一张完整知识网的纲目。关键问题在于怎么背。我的做法是每个知识点都问自己一个"为什么",比如HashMap为什么阈值是0.75,ConcurrentHashMap为什么在Java 8里改了锁粒度,Spring为什么推荐构造器注入。这些"为什么"才是面试官真正想听到的东西,也是区分背书和真懂的分水岭。
2. 笔试环节:算法与基础选择
2.1 牛客网在线笔试经验
在线笔试是很多人的第一道坎。我归纳了自己2020年参加过的笔试,题型比例大概是:选择题30%到40%,算法编程题两到三道,剩下的是简答或设计题。选择题考得特别细,经常出现"以下哪个选项是正确的"这种组合型题目,覆盖Java基础、集合、并发、JVM、Spring等,稍不注意就掉坑。
笔试经验有三条。第一,时间分配要提前规划。编程题分值最高但耗时最长,我会先快速过一遍选择题,拿不准的标记跳过,把大块时间留给编程题。第二,要提前适应牛客网的环境。在线IDE没有智能补全,很多平时依赖IDEA的人写起代码来明显变慢,建议在正式笔试前用牛客网的模拟环境做几道题热身,顺便把输入输出模板练熟。第三,重点注意输入输出格式。大厂笔试的用例经常是循环读取、多组输入,字符串要自己split和解析,不少人的算法明明对了,却卡在输入输出上,特别可惜。
2.2 手写排序算法:冒泡与快排
排序算法是笔试和面试手写代码的重灾区。冒泡排序考逻辑,快速排序考分治思想,面试官还会追问时间复杂度、空间复杂度、稳定性以及优化方案。我2020年面试时被要求手写过三次冒泡、两次快排,这个频率足够说明它的重要性。
先看冒泡排序的Java实现:
public static void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) { return; } int n = arr.length; for (int i = 0; i < n - 1; i++) { boolean swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; swapped = true; } } if (!swapped) { break; } } }里面有一个很重要的优化点:swapped标志位。如果某一轮扫描没有任何交换,说明数组已经有序,直接退出。这个点面试官经常追问,能主动写出来并解释原因,说明你真的理解了冒泡排序,而不是死记代码。
快速排序面试频率更高,核心是分区函数:
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 base = arr[left]; int i = left; int j = right; while (i < j) { while (i < j && arr[j] >= base) { j--; } while (i < j && arr[i] <= base) { i++; } if (i < j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } arr[left] = arr[i]; arr[i] = base; return i; }快排的高频追问点有几个:为什么最坏情况是O(n^2)?因为如果每次选的基准值都是最大或最小元素,分区极不平衡,退化成和选择排序一样。怎么优化?三数取中,或者随机选基准。还有一个容易忽略的细节:分区时对相等元素的处理。上面代码用>=和<=,目的是把相等的元素均匀分到两边,避免出现无限递归或死循环。这些细节能答上来,面试官会认为你有过真实的代码实践。
笔试算法题不止排序。链表反转、二叉树层序遍历、滑动窗口、动态规划里的爬楼梯和背包,都是常客。我的体会是,笔试不需要追求难题偏题,中等难度以下的题目要吃透,尤其是原题要能快速写出。因为笔试时间紧,考察的就是熟练度。
2.3 选择题里反复出现的Java基础
笔试选择题中,Java基础占比很高,而且题目出得非常细。我整理了2020年笔试里反复出现的几类:
- 运算符优先级和表达式求值。
- 面向对象特性、重载与重写的区别。
- 集合类的线程安全性。
- 字符串常量池与String、StringBuilder、StringBuffer。
- 异常体系、数组越界。
- 枚举类型、Lambda表达式。
运算符和表达式看起来基础,但笔试的坑特别多。比如i++和++i的求值顺序,==和equals的区别,String的+拼接到底走了什么逻辑。有一道高频题我印象很深:Integer a = 127; Integer b = 127; a == b结果是true,因为Integer缓存范围是-128到127;但把值改成128,结果就是false。这种题如果没看过源码,现场只能靠猜。
枚举类型也是选择题常客。比如enum能不能继承别的类?不能,因为枚举默认继承java.lang.Enum。枚举能不能实现接口?可以。枚举的构造方法能不能是public?不能,必须是私有。还有个冷门考点:values()和valueOf()是编译器自动生成的静态方法,而不是Enum类自带的。这些点平时写业务代码根本遇不到,但笔试选择题就是喜欢考。
Lambda表达式在2020年面试里越来越常见,因为Java 8已经普及多年。面试官会问Lambda的本质、函数式接口、方法引用、stream流的中间操作与终止操作。笔试里常见的是让你写出filter、map、collect的调用结果。我建议把java.util.function包里的核心接口都过一遍,包括Function、Predicate、Consumer、Supplier,以及方法引用的四种形式,这样现场不会反应不过来。
对了,排序相关的Comparator.comparing在笔试和面试手写题里很实用。比如把List按某个字段降序排序,可以写list.sort(Comparator.comparing(User::getAge).reversed())。如果想把某个特定值排在最前面,可以用Comparator.comparing(u -> u.getStatus() == 1 ? 0 : 1).thenComparing(User::getAge)。这个写法在技术社群里被问过很多次,不算难,但思路要清晰。
3. 面试重头戏:Java基础与八股文
3.1 面向对象三特性怎么回答不落俗套
面试官问"讲讲面向对象"时,大部分人都会先背"封装、继承、多态"六个字,然后就接不下去了。这种回答很容易被追问到死角。我的答法是:先给定义,再给例子,最后讲底层限制,把主动权拿在自己手里。
封装:把对象的属性和行为绑定在一起,对外暴露必要的方法,隐藏内部实现。好处是降低模块间耦合,便于维护。比如private字段配public的getter/setter就是最基本的封装。这里可以顺势提到访问控制符的作用范围,让回答更完整。
继承:子类复用父类的属性和方法,体现"is-a"关系。注意Java是单继承,但接口可以多实现。面试官通常会追问"为什么Java不支持多继承",答案是菱形继承问题:如果多个父类有同名方法,子类无法确定调用哪个。另外还要补充,子类构造器会隐式调用父类无参构造器,这也是一个常考点。
多态:同一操作作用于不同对象产生不同结果。多态实现的基础是继承、方法重写和向上转型。Java的方法调用默认是动态分派,所以运行时才决定实际调用哪个方法。讲到这层,可以自然引出JVM的方法表和invokevirtual指令,很多面试官到这里就会满意地点头。
我在面试中还遇到过"重载和重写的区别"。重载发生在编译期,方法名相同、参数列表不同,看的是静态类型;重写发生在运行期,子类覆盖父类方法,要求方法签名相同、访问权限不能更严格、返回值可以协变。额外注意:重写时子类抛出的异常不能比父类更宽泛,static方法不能重写,只能隐藏。
3.2 细节题:运算符、表达式、枚举、Lambda
第一轮面试经常出一些"看似简单"的细节题,其实是在试探基本功。比如运算符:&和&&的区别、|和||的区别。&&有短路效果,左边为false时右边不会执行;&没有短路,两边都会执行。|和||同理。还有一个容易混淆的点:equals和hashCode的约定,以及位运算在HashMap源码中的应用,比如用(n - 1) & hash计算数组下标。这些细节题和源码题经常混在一起,想答好必须自己看过源码。
枚举的考察点前面提过一些。我补一个实用场景:用枚举实现单例模式。这是《Effective Java》作者推荐的写法,因为枚举天然保证线程安全,并且反射无法破坏。如果面试时你能主动提出这个点,会被明显加分。笔试中还出现过"枚举不能继承类,但可以实现接口"这类判断题,处理起来也很简单,记住枚举的基类是java.lang.Enum就够了。
Lambda的考察点不能停留在语法层面。面试官问"Lambda是怎么实现的"时,要能回答出"函数式接口加invokedynamic"这层。Java 8的Lambda表达式经过编译后会生成一个静态方法和invokedynamic指令,运行期通过LambdaMetafactory生成函数式接口的实例。能讲到这里,说明你读过字节码或源码,而不是只会写箭头语法。我建议有时间可以用javap反编译一个Lambda方法看看字节码,面试时的底气完全不一样。
3.3 集合容器的底层逻辑
集合这块,面试官特别喜欢问"为什么有的集合线程不安全、有的安全"以及"底层数据结构是什么"。我把高频考点按容器列了一遍:
- ArrayList:底层是Object数组,默认容量10,扩容后变成原来的1.5倍。查询快、增删慢,线程不安全。
- LinkedList:底层是双向链表,增删快、查询慢,随机访问需要遍历。
- HashMap:底层是数组加链表加红黑树,初始容量16,负载因子0.75,链表长度超过8且数组长度大于等于64时转红黑树,扩容为原来的两倍。
- ConcurrentHashMap:Java 8是用CAS加synchronized保证线程安全,锁粒度精确到桶,比Java 7的分段锁并发度更高。
- HashSet:底层就是HashMap,value统一为同一个Object对象。
- LinkedHashMap:在HashMap基础上维护了双向链表,适合实现LRU缓存。
HashMap是当之无愧的第一高频考点。我2020年面试的每一家公司都问了HashMap,而且至少追问三层。第一层是底层结构和put流程,put大概分四步:计算hash、定位桶、判断是否哈希冲突、冲突后判断是链表还是红黑树。第二层是为什么负载因子是0.75,这是时间和空间的折中,太高会增加冲突,太低会浪费空间,源码注释里给出了基于泊松分布的统计依据。第三层是为什么链表转红黑树的阈值是8,因为随机哈希下链表长度到达8的概率极低,大概千万分之六,如果真到了8,说明哈希函数可能有问题,这时用红黑树来抵御极端冲突。
把这几个"为什么"都答下来,HashMap这一关才算真正过了。
4. JVM与并发:拉开差距的深水区
4.1 内存结构、GC和类加载
JVM是2020年大厂面试的必考板块,被问到的概率高得惊人。核心就三大块:内存结构、垃圾回收、类加载。
内存结构要能画出六个区域:程序计数器、虚拟机栈、本地方法栈、堆、方法区(Java 8以后改为元空间)、运行时常量池。特别要区分线程私有和线程共享:程序计数器、虚拟机栈、本地方法栈是线程私有,堆和方法区是线程共享。面试官喜欢追问"栈上对象会不会被GC""元空间会不会OOM"这类问题,答的时候要明确:栈上一般没有GC,但栈帧会随着方法结束弹出;元空间用本地内存,如果动态生成类过多,一样会OOM。
垃圾回收的核心是判断对象是否已死。引用计数法实现简单,但有循环引用问题,所以主流JVM用可达性分析。从GC Roots出发遍历,不可达对象可回收。GC Roots包括虚拟机栈中的引用、静态属性引用、常量引用、本地方法栈中的引用。分代收集的思路也要会讲:新生代(Eden加两个Survivor)用复制算法,老年代用标记整理或标记清除。常见的收集器CMS和G1要能说清楚适用场景和优缺点,尤其G1的目标是把STW时间控制在可预测范围内。
类加载机制同样是高频题。双亲委派模型要讲透:一个类加载器收到类加载请求时,先不自己加载,而是把请求委托给父类加载器,最终由Bootstrap ClassLoader处理,父类加载不了才由子类加载器自己加载。好处是避免相同类被重复加载,也防止核心类库被篡改。面试官常追问"什么场景需要自定义类加载器",比如热部署、Tomcat的Web应用隔离、OSGi模块化等。能答出"打破双亲委派"的实践场景,比如JDBC用SPI加载驱动,会让回答更完整。
4.2 OutOfMemoryError排查实战
要说2020年面试里最常被问到的异常,OutOfMemoryError一定排在前列,而且很多公司要求你结合项目讲一次真实的OOM排查经历。
先区分一个常见误区。IDE里运行Spring Boot项目时,IDEA偶尔会提示:
java: OutOfMemoryError: Insufficient memory这个错误其实是IDE编译器或Maven构建进程的堆内存不足,不一定是应用本身的JVM堆不够,所以解决办法是调整IDEA自身的内存设置,而不是改-Xmx。面试时如果把这个和Java堆OOM混为一谈,容易被面试官当场纠正。
项目里真正出现的OOM主要有这几种:
| 报错类型 | 常见原因 | 排查方向 |
|---|---|---|
| Java heap space | 对象堆积、大对象过多 | jmap -histo,MAT分析 |
| Metaspace | 动态生成类过多 | 检查CGLIB代理、反射调用 |
| unable to create new native thread | 线程数超过系统限制 | 查看线程栈,调整系统线程数配置 |
| GC overhead limit exceeded | GC频繁但回收效果差 | 观察GC日志,优化堆大小或代码 |
讲排查经历时,我一般按这个套路:先看异常类型,再用jps找到进程ID,用jmap -heap看堆使用情况,用jstat -gcutil观察GC频率,最后用jmap -dump:format=b,file=heap.bin导出堆转储,用MAT分析大对象和对象引用链。这套流程我在项目里实际操作过,面试时讲出来很有说服力。
我有一个真实的案例。一个定时任务每天凌晨跑,运行两周后突然OOM。第一次处理时我只是把-Xmx调大了,结果只是延长了崩溃时间,并没有解决问题。后来我在崩溃前导出了堆转储,用MAT打开后发现问题出在一个缓存Map上:任务不断向Map里放数据,但清理线程因为锁竞争没有及时执行,Map越滚越大。定位到根因后,把缓存换成带过期时间的本地缓存,问题才彻底解决。
这个案例让我记住两件事。第一,调大堆内存是应急,不是根因,OOM一定要找到谁占用了内存。第二,排查顺序可以先用jmap -histo观察类实例数量,快速锁定可疑对象,不一定每次都要直接上MAT。
4.3 并发编程高频考点
并发是Java后端面试的另一个深水区。2020年大厂问到的并发题集中在这些类:synchronized和ReentrantLock的区别、volatile能保证什么、CAS是什么且有啥缺点、线程池核心参数和执行流程、ThreadLocal原理和内存泄漏、CountDownLatch和CyclicBarrier的区别。
先说synchronized和ReentrantLock。synchronized是JVM层面的关键字,ReentrantLock是JDK层面的API。Java 6之后synchronized引入了偏向锁、轻量级锁、重量级锁的升级过程,性能并不差。ReentrantLock的优势在于支持可中断、支持公平锁、可以绑定多个Condition。但多数业务场景能用synchronized就用synchronized,代码简洁,不容易出问题。
volatile是最容易被讲错的点。它保证可见性和有序性,但不保证原子性。volatile变量每次读取都从主内存读取,写入后立即刷新到主内存,并通过内存屏障禁止指令重排序。经典应用是双重检查锁定的单例模式,单例字段必须要加volatile,否则会发生指令重排序,导致某个线程拿到未初始化完成的对象。
线程池是面试官最爱问的项目场景题。核心参数有七个:核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。执行流程要能完整说下来:提交任务时先判断核心线程是否满,没满就创建新线程执行;满了放队列;队列满了并且核心线程也满,就创建非核心线程;最大线程数也满了,走拒绝策略。拒绝策略有四种:AbortPolicy抛异常、CallerRunsPolicy由调用者执行、DiscardPolicy直接丢弃、DiscardOldestPolicy丢弃最老的任务。
面试官还会追问线程池参数怎么设置。这里可以按任务类型区分:CPU密集型任务的核心线程数可以设置为CPU核数加1,因为很少阻塞;IO密集型任务由于有大量等待,核心线程数可以设置为CPU核数的两倍甚至更多。这类实战问题如果能结合自己项目的压测情况讲,面试官会更认可。
5. 框架与项目:从八股到场景题
5.1 Spring核心设计思想
Spring在2020年Java大厂面试中也是必考,高频题包括IOC和AOP的理解、Bean的生命周期、Spring事务的传播行为与失效场景、Spring Boot的自动配置原理。
IOC控制反转可以从"谁控制谁"讲起。传统开发中对象由自己new,控制权在程序员;IOC把对象的创建和管理交给容器,控制权反转了,我们只需要声明依赖。DI依赖注入是IOC的一种实现方式,有构造器注入、Setter注入、字段注入三种,推荐构造器注入,因为它能保证依赖不为空且不可变,也方便单元测试。
AOP的核心是动态代理。Spring AOP默认对接口使用JDK动态代理,对类使用CGLIB代理。从Spring Boot 2.x开始,spring.aop.proxy-target-class默认为true,即默认使用CGLIB。AOP的典型场景是日志、事务、权限校验等横切逻辑。这里可以提一句:aop失效的常见原因是类内部调用,因为内部调用的this对象不是代理对象。
Bean生命周期是面试官喜欢画图追问的题目。简化版是:实例化、属性填充、初始化(InitializingBean、@PostConstruct)、使用、销毁(DisposableBean、@PreDestroy)。完整版要提到BeanPostProcessor,它是Spring扩展机制的核心,AOP就是通过BeanPostProcessor在Bean初始化后创建代理对象的。能提到BeanPostProcessor,说明你看过Spring源码。
Spring事务的失效场景是项目经验的试金石。方法内部自调用、方法不是public、异常被catch住没向上抛、抛出了检查异常而事务默认只回滚RuntimeException,这些都是经典案例。面试官问这些,其实是想确认你在真实项目里写过事务代码,而不是只背过注解。
5.2 分布式与微服务场景题
到了第二轮或主管面,分布式是绕不开的话题。高频考点有分布式锁实现(Redis、Zookeeper)、分布式事务(2PC、TCC、消息最终一致性、Seata)、服务降级和熔断限流、消息队列可靠性、Redis缓存穿透击穿雪崩。
2020年大厂特别爱考缓存三大问题。缓存穿透是查询一个不存在的数据,请求直接打到数据库,解决思路是布隆