我帮学弟学妹做模拟面试时,开场常说一句话:校招的Java面试题,翻来覆去就那10道,但很多人其实挂在第1道。这不是夸张,而是面试官视角和候选人视角之间存在信息差。你在背Java八股文的时候,他正在捡你话里的逻辑漏洞。这篇备战指南,我把最近校招里Java面试必问的10道题整理出来,从Java基础到常见考点逐条拆解,适合正在准备校招、以及被Java面试题折磨过的同学对照着查漏补缺。目标只有一个:让你在面试现场不仅答得出,还能答得让面试官眼睛一亮。
1. 面试官聊起"Java八股文"时,他到底在面什么
1.1 校招基础题为什么永远是主战场
很多同学不理解,Java岗校招为什么总在问基础题,项目明明也准备了。我换个角度给你解释:校招候选人几乎都是应届生,项目来源高度雷同——秒杀系统、商城系统、后台管理系统,十个里有八个是培训班或网课项目。面试官没法从这些项目里看出真实水平,而算法题又只能筛掉一部分人,剩下的区分度全靠Java基础题来拉。
这里有个很关键的认知误区:基础题不是让你背答案,而是面试官用来搭“追问阶梯”的。比如问你HashMap,他可以从hash寻址追到hashCode和equals的关系,再追到对象比较,再追到线程安全问题,一路问下来,你的知识深度和思维边界就被摸清了。所谓“八股文”,是因为很多人只背了第一层答案,第二层、第三层一问就露馅,才被叫成八股。
1.2 常考的10道题画像
根据我参与的面试场次和近期的面经反馈,我把Java校招出现频率最高的10道题整理成了一张表。你可以先对着表自测一遍,看看哪几道是能一口气讲满三分钟的。
| 序号 | 题目 | 核心考点 | 出现频率 |
|---|---|---|---|
| 1 | 面向对象三大特性 | 封装、继承、多态的本质与应用 | 极高 |
| 2 | ==和equals的区别、hashCode为什么要重写 | 对象比较、散列原理 | 极高 |
| 3 | HashMap底层原理 | 数组链表红黑树、put流程、扩容 | 极高 |
| 4 | ArrayList和LinkedList的区别 | 底层结构、扩容机制、复杂度 | 极高 |
| 5 | JVM内存区域与对象创建过程 | 运行时数据区、对象分配 | 高 |
| 6 | 常见的OOM场景与排查思路 | 内存溢出、日志分析工具 | 高 |
| 7 | volatile与synchronized的区别 | JMM、可见性、原子性、锁机制 | 高 |
| 8 | 线程池核心参数与执行流程 | ThreadPoolExecutor、拒绝策略 | 高 |
| 9 | 异常体系与try-catch-finally细节 | checked/unchecked、资源关闭 | 中高 |
| 10 | 手写排序算法与复杂度分析 | 冒泡、快排、边界条件 | 中高 |
另外还有三个高频配角,几乎每次都藏在追问里:枚举实现单例的原理、lambda表达式为什么只能访问effectively final变量、数组越界异常属于哪类异常。它们单拎出来不算一道独立大题,但答不好照样扣分。
2. 面向对象与集合高频题:这样答才能和背答案的人区分开
2.1 面向对象三大特性:别只背定义
这题看似简单,但绝大多数候选人只会说“封装就是私有化,继承就是extend,多态就是重载重写”,说完就没了。这种答法在面试官眼里等于没答。
我建议按“定义+本质+例子+项目应用”四个层次来讲。封装,本质是信息隐藏和访问控制,目的不是把字段私有化,而是通过暴露稳定的方法接口来降低模块间的耦合。你可以举例子说订单状态字段不允许外部直接改,必须通过cancelOrder()这类方法变更,这样状态流转规则都收拢在一个地方,不容易被改乱。继承,本质是抽取公共代码形成is-a关系的复用机制,但要主动补一句“实际开发中组合优于继承,继承层级太深会带来维护灾难”,这一句就能拉开和机械背诵的距离。
多态是最能区分的点。要主动拆分编译期多态和运行期多态:重载是编译期静态绑定,重写是运行期动态绑定。运行时多态的实现依赖方法表,JVM在真正调用方法时根据对象的实际类型找到对应的方法入口。讲到这里再带一个例子,比如策略模式里把支付行为抽象成接口,微信支付和支付宝支付各自实现,调用方持有接口引用,新增一种支付方式时不用改动业务代码。这一套讲下来,面试官会直观感觉到你是真用过多态,而不是背过定义。
2.2 ==与equals、hashCode:一道小题看底层功底
这道题的技术点不深,却是面试官最常用的“探针”。问题从“==和equals有什么区别”开始,如果你答到了String的equals被重写为内容比较,他会马上追问“那String的hashCode呢?为什么重写equals还要重写hashCode?”
关键逻辑是这样一条链:hashCode方法返回的是一个int散列值,HashMap和HashSet这类散列集合靠它定位桶的位置。而equals用来判断两个对象是否逻辑相等。Java有一条硬性约定:两个对象equals相等,hashCode必须相等;两个对象hashCode相等,equals不一定相等。如果只重写equals不重写hashCode,就会发生一个经典事故——你用业务唯一标识new了两个对象,逻辑上明明相同,但HashMap把它们放进了两个不同的桶,get的时候找不到对方。
我给一个面试现场可以直接说的例子。假设有个User类,用id作为业务唯一标识:
public class User { private String id; private String name; public User(String id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return id.equals(user.id); } @Override public int hashCode() { return id.hashCode(); } }你可以告诉面试官,重写equals的同时重写hashCode,保证了同一个id的User对象不管new几次,在HashMap里都只会落进同一个桶。String之所以能直接用equals比较内容,正是因为String类自己重写了equals和hashCode。这套逻辑讲清楚,“背答案”和“懂原理”的区别就出来了。
2.3 HashMap底层原理:校招面试的“必考压轴”
HashMap在Java面试里的地位不用我多说。面试官一般从“HashMap在JDK 8里的底层结构是什么”开始,一路追到put流程、扩容机制、为什么长度是2的幂次、以及为什么线程不安全。
JDK 8之后底层是“数组+链表+红黑树”,数组每个位置叫桶。put一个key-value时,流程大概是:先对key的hashCode做一次扰动计算(高16位和低16位异或),然后用(n - 1) & hash算出桶的位置;如果这个桶是空的,直接放进去;如果桶里已经有链表或红黑树,就遍历找到key相同的节点做覆盖,找不到就新增;当链表长度超过8且数组长度达到64时,链表转红黑树。接下来检查元素个数是否超过threshold(capacity * loadFactor),超过就扩容到原数组的两倍。
扩容为什么是2倍、数组长度为什么是2的幂次,这是面试官特别爱问的细节。因为计算桶位置时用的是tab[(n - 1) & hash],只有当n是2的幂次时,n - 1的二进制才是末尾全1,这时位运算等价于取模,能均匀散列;同时位运算比取模快得多。如果数组长度不是2的幂次,用位运算就会产生大量碰撞,查找效率急剧下降。默认容量16、负载因子0.75,这是空间利用率和查询效率的权衡结果:负载因子太高,碰撞变多;太低,数组太稀疏浪费内存。
最后一定要主动讲线程安全。HashMap本身不是线程安全的,JDK 7在并发扩容时采用头插法,多线程同时扩容可能形成环形链表,导致get死循环;JDK 8改成尾插法,环形链表问题没了,但并发put仍可能丢数据。正确做法是并发场景用ConcurrentHashMap。ConcurrentHashMap的原理能展开讲多少就讲多少:JDK 8里抛弃了分段锁,改用CAS加synchronized锁住桶的头节点,锁粒度更细,并发度更高。这块如果也能答出来,面试官对你的评价会明显上升一个档次。
2.4 ArrayList和LinkedList:说“ArrayList快”是会扣分的
很多同学被问到“ArrayList和LinkedList有什么区别”,张口就是“ArrayList查询快、增删慢,LinkedList增删快、查询慢”。这个说法在面试里至少要扣一半分,因为它忽略了一个关键前提:复杂度是分场景的。
正确的答法是先讲底层结构:ArrayList底层是连续数组,LinkedList底层是双向链表。再分场景讲复杂度。查询方面,ArrayList按索引随机访问是O(1),LinkedList需要从头或尾开始遍历,是O(n),但LinkedList的get(int index)实现里做了一个小优化,会先判断index靠近头部还是尾部,从近的一端开始找,不过复杂度依然是O(n)。增删方面,ArrayList在尾部添加是均摊O(1),但在中间插入需要System.arraycopy整体移动后半段,是O(n);LinkedList如果在已知节点位置插删是O(1),问题在于要先找到那个位置,这个查找本身是O(n)。
还要补充内存占用和扩容。ArrayList默认容量10,每次扩容为原来的1.5倍,扩容时要创建新数组并复制元素。1.5倍这个数字值得讲为什么:扩容倍数太高浪费内存,太低则频繁扩容多次拷贝数组,1.5倍是经过权衡的经验值,不是拍脑袋。LinkedList的每个节点要额外维护prev和next两个引用,还要有Node对象头,内存开销明显更大,而且节点在内存中不连续,对CPU缓存不友好。
最后收一个反直觉结论:实际业务里大多数场景ArrayList是更优选择,因为它内存连续、缓存友好、尾部追加性能好;LinkedList的优势场景非常窄。说出来以后再加一句,“我们项目里日志列表、订单明细这类数据都是ArrayList,LinkedList只在需要频繁在已知位置插入删除时用”,面试官会觉得你有工程判断力。
3. JVM与并发必问题:从OOM报错到线程安全的完整答法
3.1 JVM内存区域:画图之前先把逻辑理清
JVM内存区域几乎是JVM环节的必问题。面试官通常会说“说一下JVM运行时数据区”。这时候你先在脑子里把两种区域分清楚:线程私有的有程序计数器、虚拟机栈、本地方法栈;线程共享的有堆和方法区。
程序计数器是当前线程执行字节码的行号指示器,上下文切换后靠它恢复执行位置,不会OOM。虚拟机栈描述Java方法执行的内存模型,每个方法从调用到结束对应一个栈帧入栈出栈,栈帧里有局部变量表、操作数栈、动态链接、方法返回地址。局部变量表存放基本类型和引用类型,栈深度不够会抛StackOverflowError。本地方法栈服务的是native方法。
线程共享区域里,堆是存储对象实例的主要区域,也是GC的主要阵地。堆内部一般再分新生代和老年代,新生代又分Eden区和两个Survivor区,比例默认8:1:1。方法区在JDK 8之后被元空间Metaspace取代,运行时常量池也搬了过去,最大的变化是元空间使用本地内存,不再受JVM堆大小限制,所以Class元数据太多时的溢出类型从PermGen space变成了Metaspace。
讲完区域之后主动补一段“对象创建过程”,这是加分项。一个Java对象从new开始要经过5步:类加载检查、分配内存、内存空间初始化零值、设置对象头、执行<init>构造方法。分配内存时有两种方式,指针碰撞和空闲列表,取决于堆内存是否规整,而是否规整又取决于垃圾回收器是标记整理还是标记清除。这里的逻辑链条很长,但每一步都能接住面试官的追问,足以证明你真读过虚拟机的原理,不是背的术语。
3.2 OOM报错与排查思路:从热搜报错说起
最近有个跟Java相关的热搜词是java: outofmemoryerror: insufficient memory,不少同学开发环境里跑着跑着就弹这个错,第一反应是“内存不够,加大堆内存”。这确实是一种原因,但不全面。这个报错本质是Java进程向操作系统申请内存失败,可能因为物理内存不足、32位JVM地址空间不足、堆大小设置过大超出机器内存、或者容器里给JVM分配的内存超出容器限制等。
校招面试里更常问的是OOM的常见类型。我把重点列一下:Java heap space是堆空间满了,对象太多且GC回收不掉;GC overhead limit exceeded是GC回收效率极低,CPU几乎都在做GC但回收不了几个对象;Metaspace是元空间满了,大量动态生成类;Direct buffer memory是直接内存用完,NIO使用不当会触发;unable to create new native thread是线程数超限,无法再创建操作系统线程。StackOverflowError严格说属于栈溢出不是OOM,但也会被拿出来一起问。
排查思路比具体命令更重要。你先说:遇到OOM第一步是看错误日志,确认是堆内存还是非堆内存的问题;如果是堆溢出,保留现场,用jmap -dump:format=b,file=heap.hprof <pid>导出堆转储文件,再用MAT或jvisualvm分析哪个对象占用了大量内存、有没有内存泄漏;如果是线程问题,用jstack看线程栈;同时可以用jstat -gcutil <pid>看GC情况和各区占比。面试不需要背一堆命令,但把“日志定位-堆转储-工具分析-定位大对象/泄漏根源”这条链路讲完整,面试官就知道你处理过线上问题。
3.3 volatile与synchronized:一道题串起整个JMM
“volatile和synchronized的区别”这题,要想答好,必须先把JMM(Java内存模型)讲清楚。简单说,Java里每个线程有自己的工作内存,线程读取共享变量时先把主内存的值拷贝到工作内存,修改后再刷回主内存。这个机制导致了可见性问题:一个线程改了值,另一个线程可能还读着旧值。synchronized通过加锁保证同一时刻只有一个线程执行临界区代码,但它的重是用互斥换来线程安全;volatile则是轻量级的,它做了两件事:保证变量修改后立即刷回主内存,其他线程读取时强制从主内存重新读;同时禁止编译器和处理器对这个变量相关指令进行重排序。
关键要强调:volatile不保证原子性。面试官最爱追问的场景就是volatile int count,多个线程做count++,结果依然不对。因为count++是三步操作——读取、加1、写回,volatile只能保证这三步各自在内存层面可见,但不能保证这三步整体不被打断。解决办法是用AtomicInteger或synchronized。另外要说一个典型正确用法:DCL双检锁单例里,用volatile修饰instance字段,防止指令重排导致拿到未初始化完成的对象。这块能主动讲出来,面试官会非常满意。
对比表格可以这样收尾:
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | 保证 | 保证 |
| 原子性 | 不保证 | 保证 |
| 可重入 | 不支持 | 支持(重量级锁可重入) |
| 锁类型 | 无锁 | 偏向锁/轻量级锁/重量级锁 |
| 性能开销 | 较小 | JDK 6优化后差距缩小 |
| 典型场景 | 状态标志位、DCL单例 | 多线程写共享数据 |
3.4 线程池核心参数:面试官的爱与恨
线程池是并发环节的高频考点,几乎每次都会问到。面试官喜欢问的版本是:“线程池有哪些核心参数?当任务提交后,执行流程是怎样的?”
我先说流程,这题有标准答案。一个任务提交给ThreadPoolExecutor后,按这个顺序判断:当前工作的线程数是否小于核心线程数,小于就直接创建核心线程执行任务;否则尝试把任务放进阻塞队列;队列满了再判断是否小于最大线程数,小于就创建非核心线程执行;如果连非核心线程也满了,就执行拒绝策略处理任务。核心参数有7个:corePoolSize、maximumPoolSize、keepAliveTime、TimeUnit、workQueue、threadFactory、RejectedExecutionHandler。这里的本质是:核心线程、阻塞队列、最大线程数、拒绝策略,相当于一套四级缓冲机制,逐层兜底。
第二个高频追问是“为什么不能直接用Executors创建线程池”。这个答案是确定的:Executors提供的FixedThreadPool和SingleThreadPool用的阻塞队列是LinkedBlockingQueue,默认容量是Integer.MAX_VALUE,任务一多就疯狂堆积,最终可能OOM;CachedThreadPool的最大线程数是Integer.MAX_VALUE,同一时间创建大量线程会耗尽系统内存。所以规范做法是手动new ThreadPoolExecutor,明确指定核心线程数、队列容量和拒绝策略。最后一个追问是四种拒绝策略,这个直接背:AbortPolicy直接抛异常、CallerRunsPolicy让提交任务的线程自己执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃最旧的任务再重新尝试执行。
我在模拟面试里见过很多人把参数背得很熟,但问到“线上线程池的corePoolSize应该设置多少”就卡住。这里给个大方向:CPU密集型任务设置成CPU核数加1,IO密集型任务可以设置成CPU核数乘2,因为IO操作阻塞时线程可以让出CPU给别的任务。这不是标准答案,但能说出这个考量,说明你理解配置背后的资源模型。
4. 细节题里最容易失分的三个地方:异常、枚举、lambda
4.1 异常体系:checked和unchecked才是关键分水岭
异常这题直接问的方式比较少,但它是各种追问的常客。比如手写代码时发生了ArrayIndexOutOfBoundsException,面试官随口问一句“这个异常是checked还是unchecked”,很多人就开始懵。答案应该是:它是RuntimeException的子类,属于unchecked异常,编译器不会强制你捕获。
Java的异常体系顶层是Throwable,下面分Error和Exception。Error是严重错误,比如OutOfMemoryError、StackOverflowError,程序本身无法恢复,不应该捕获。Exception再分两类:checked异常,编译器强制要求捕获或声明抛出,比如IOException、SQLException;unchecked异常,也就是RuntimeException及其子类,编译器不强制处理,比如NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException。
再往下会问到try-catch-finally。有两个细节值得背准:第一,finally里的代码无论是否出现异常都会执行,唯一不执行的情况是在try或catch里执行了System.exit(1),或者JVM进程崩溃;第二,如果finally里有return,它会覆盖try或catch里的return值,这是很多隐蔽bug的来源。最后补充try-with-resources,JDK 7之后实现了AutoCloseable的资源可以写在try小括号里,作用域结束后自动关闭,本质是编译器生成finally调用close方法,比你手写finally更安全。这几层讲下来,异常这题就覆盖完整了。
4.2 枚举:平时用得少,面试问得巧
枚举在项目里使用频率不高,但面试官特别喜欢拿来考原理,因为它能区分“只是会写”和“真研究过”。
首先要明确枚举的本质:一个enum类编译后继承自java.lang.Enum,是final类,枚举常量实际上是类的静态实例。接着可以讲经典应用——用枚举实现单例。这是《Effective Java》里推荐的方案,原因有三点:枚举构造器私有,外界无法new;枚举实例由JVM保证只创建一次,天然线程安全;枚举在序列化时JVM通过特殊逻辑保证实例唯一,并且反射也不容易破坏单例。相比Double-Checked Locking这种写法,枚举单例代码极简又安全,是面试加分项。
public enum DataSourceSingleton { INSTANCE; public void doSomething() { // 业务逻辑 } }然后再延伸一个常见追问:枚举和普通class有什么区别?答三点就够了:枚举常量自带名字和序数,name()和ordinal()方法直接可用;枚举可以有自己的字段、构造函数(但构造函数必须是私有的)、抽象方法;switch语句支持配合枚举使用。如果你项目里用过枚举来做状态机,比如订单状态从待支付到已支付到已完成,每一步的转变规则由枚举自己管理,这个场景一定要讲出来,比单纯背概念强太多。
4.3 lambda与Stream:从“会用”到“讲清楚为什么”
lambda和Stream在项目里用得很多,面试却很少作为独立大题目,更常见的形态是:你代码里顺手写了一个lambda,面试官指着问“这个lambda捕获的变量为什么必须是final或effectively final”。
这道题的原理值得讲透。lambda表达式本质上是一个函数式接口的实例,它在创建时会把外部局部变量的值捕获到自己的上下文里。为什么不能捕获一个会被修改的变量?因为并发环境下,如果变量被多线程修改,就会出现数据竞争,编译器无法保证安全。如果lambda需要引用一个可变的外部状态,更好的做法是把它包装成AtomicInteger或放进一个容器对象,或者改为方法参数传入。能讲到这个程度,面试官不会再追问。
Stream部分,记住几个核心概念就行:中间操作是惰性的,终端操作触发执行;map做转换、filter做过滤、collect聚合结果;并行流parallelStream底层是JVM的ForkJoinPool公共线程池,不适合所有场景,数据量小反而更慢。还有一个进阶点:lambda和Stream的性能没有传说中那么差,JIT编译后往往能接近手写循环,但免去了大量样板代码。另外如果聊到异步,可以提CompletableFuture,比如supplyAsync配合thenApply和exceptionally做异步链路。这段讲得自然一些,会让对方觉得你是有实战积累的,不是刚刷了两道题。
5. 手撕代码环节:面试官最常让写的排序题与边界处理
5.1 冒泡排序:人人都能写,优化才是加分项
手撕排序是校招Java岗的高频现场题。我见过太多人冒泡排序写得磕磕绊绊,或者写对了但完全没考虑过优化。给你一个面试现场可以直接用的版本:
public 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; } } }这个版本有两个加分点:第一是j < n - 1 - i,每轮结束后最后i个元素已经在正确位置,不需要再比较;第二是swapped标记,如果某一轮循环里没有发生任何交换,说明数组已经有序,直接提前退出。最坏时间复杂度O(n^2),最好情况(数组本身有序)优化后是O(n)。
写完之后面试官可能会追问“这个算法稳定吗”。答案是想一下:冒泡排序只有在严格大于时才交换相等元素的位置,所以相等元素的相对顺序不变,是稳定排序。这里能答出来,说明你理解“稳定性”这个概念而不只是会背。
5.2 快速排序:核心在partition和边界
快排也是手写高频题。很多同学能背出思路,但一写就出现数组越界,或者死循环。核心是partition函数,我这里给一个比较稳妥的写法:
public void quickSort(int[] arr, int left, int right) { if (left >= right) { return; } int pivotIndex = partition(arr, left, right); quickSort(arr, left, pivotIndex - 1); quickSort(arr, pivotIndex + 1, right); } private int partition(int[] arr, int left, int right) { int pivot = arr[right]; int i = left; for (int j = left; j < right; j++) { if (arr[j] < pivot) { swap(arr, i, j); i++; } } swap(arr, i, right); return i; }这里选最后一个元素做基准,i指针记录比基准小的元素的边界,j遍历数组,最后把基准放到i的位置。写的时候最容易翻车的点有两个:递归的结束条件是left >= right,一个等号都不能少,否则会无限递归;partition中的循环条件是j < right,不能写成j <= right,否则基准值参与比较会乱套。
写完主动分析复杂度:平均时间复杂度O(n log n),最坏O(n^2),发生在数组已经有序且每次选最大或最小做基准时。优化思路是随机选基准或三数取中。稳定性方面快排是不稳定的,因为partition过程会跨越式交换元素。这里可以补充对比:冒泡排序最好O(n)、最坏O(n^2),快排平均O(n log n);数据规模小的时候插入排序反而更快,工程中JDK的Arrays.sort在规模小时会切回插入排序。
5.3 面试现场写代码的三个习惯
我在面试里见过不少候选人,代码本身没问题,但过程一塌糊涂。给你三个能让面试官对你好感大增的习惯:
第一个习惯是动手前先聊思路。哪怕面试官不给提示,你也应该说,“我的思路是先选基准、做分区、再递归,时间复杂度平均O(n log n),可以吗?”这样既给了面试官纠偏的机会,也展示了你思考问题的路径。第二个习惯是变量命名清晰,别用a、b、c这种无意义名字,pivot、left、right、swapped一写出来,观感完全不同;顺手在关键一两条逻辑上加注释,效果更好。第三个习惯是写完主动测边界,至少把空数组、单元素数组、全部相同的数组这三个情况在脑子里过一遍,跟面试官说“我检查一下这段代码对空数组和单元素数组会不会越界”,这个动作非常加分。
如果现场出现了ArrayIndexOutOfBoundsException,面试官可能会放大你的慌张程度。这时候一定要稳住,先从索引变量开始排查,从左边界到右边界逐步确认,说一句“我怀疑是递归边界少了一个等号,我先检查这里”。这比沉默着改代码强一百倍。
6. 除了背题,校招Java备战的几条真实经验
6.1 从一道题展开一棵知识树
备战校招最忌讳的是按题号刷题,刷完就忘。我推荐的策略是“一题生树”:每遇到一道题,就把它当作根节点,把相关的知识点全部挂上去。以HashMap为例,它能长出的分支有:hashCode与equals的约定、散列冲突的解决方式、红黑树和链表的分界条件、扩容为什么是2的幂、ConcurrentHashMap的分段锁演进、CAS原理、synchronized锁升级,再往外还能连到ThreadLocal、弱引用、ThreadLocalMap的内存泄漏问题。一道题就能覆盖集合、并发、JVM三个大模块,比你零散刷几十题高效多。
面试官在追问时有个习惯:他问的问题都是你答案里的“下一层”。如果你回答HashMap时主动说“JDK 8用尾插法解决了JDK 7头插法死循环问题”,他大概率会顺着问“JDK 7为什么头插法会死循环”。你把这棵树准备完善了,每一个主动抛出的知识点都是你控制面试节奏的工具。反过来,如果你讲一个点就断层,反而会暴露知识面窄。
6.2 项目介绍和基础题怎么穿插
基础题答得好只能保证你“合格”,项目讲得好才能让你“出众”。但校招项目普遍简单,怎么办?我的建议是用STAR法则把项目体验拉满:背景是什么、你在里面承担什么角色、遇到什么具体困难、你怎么解决的、最终效果如何。尤其是“困难”部分,很多同学只讲功能不讲困难,面试官没法评估你的真实能力。
基础题和项目题不是割裂的,可以互相穿插。比如面试官问“你项目里并发量怎么控制的”,你可以顺着说“我们用了线程池做异步削峰,当时配置了核心线程数8、最大16、队列容量1000,用的是AbortPolicy,因为我考虑过内存泄漏的问题”。这样你既回答了项目问题,又提前把线程池考点抛出来了。但注意别太刻意,如果面试官没问,不要硬把话题扯过去,自然优先。
顺带说一个环境层面的坑:最近不少来模拟面试的同学在本地开发环境出问题,最常见的是java: 警告: 源发行版 17 需要目标发行版 17这类报错,本质是JDK版本不一致或Maven编译级别配置错了。这种问题好好奇解决一下就行,但面试前一定把环境变量、JDK版本、IDE编译选项都自测一遍,别等到面试官让你共享屏幕写代码时,现场浪费大量时间配置环境。
6.3 模拟面试和复盘,比刷题更重要
最后说一个我特别坚持的观点:刷一百道题不如做三次模拟面试。校招面试本质上是一场限时高压交流,你不练习语言组织,到了现场就会“听懂了但说不好”。我在帮别人做模拟面试时发现一个规律:大部分同学第一次模拟时,连“HashMap的put流程”都讲得前言不搭后语;但复盘一次、自己脱稿再讲一遍之后,第二次明显逻辑清晰了。真实面试和模拟面试的差别,只是紧张程度更高而已。
复盘的方法很简单:给自己录音,回放时听三个点——有没有答非所问、有没有频繁用“那个”“就那个”来拖延、有没有讲着讲着突然越过关键环节。把这些问题标记出来,逐个击破。另一个建议是找水平比你高的同学或学长学姐做追问,目的不是背答案,而是适应“被追着问”的状态。
我最想强调的是:面试是交流,不是背课文。面试官问10道题,真正的目标不是听标准答案,而是看你敢不敢跟他对视、能不能在卡壳时坦诚说“这块我研究过,让我稍微整理一下思路”,以及在被追问到最后时还有没有继续思考下去的韧性。这10道题是起点,不是终点。把这里面的每一道题都变成你知识树的根节点,后面再去啃Spring、数据库、Redis时,你会发现自己理解框架的速度都变快了——因为Java基础才是那个真正的地基。