不扯虚的。这几年我筛过不少简历,也面过上百个Java候选人,有个感受越来越强烈: 八股文不是没用,而是"死背"没用 。真正拉开差距的,不是谁能把HashMap的源码背得更完整,而是谁能在高压面试现场,把一套知识讲成自己的东西。但反过来说,完全不准备八股文,临场全靠"工作经验"来编,翻车概率也极高。所以我花了前后几个月,把自己手头散落的笔记、面别人的记录、被面时记下的问题,全部重新过了一遍,整理成了一套按"面试官出题逻辑"编排的背诵版。这份东西不是把网上的面试题抄一遍,而是把每个高频考点的"面试官想问什么""怎么答能拿分""答错会死在哪个环节"都标了出来。这篇博文就是把整理过程中的核心思路、高频考点、以及那些特别容易踩的坑分享出来,希望能给正在准备Java面试的人一个能直接上手的抓手。

1. 先聊清楚:八股文到底该怎么背,才不会白背

好多人一提到八股文就摇头,觉得面试问的都是工作中用不到的东西。这个观点对了一半。 工作中确实不需要手写红黑树,但面试官考红黑树,考的不是你能不能写出来,而是你有没有在遇到HashMap性能问题时能往这个方向想的能力。 这是两码事。

我整理这套背诵版时,给自己定了个原则:每个考点必须回答三个问题——"面试官为什么问这个""最容易被追问的坑在哪""怎么用一句话讲明白"。我举个例子你就懂了,比如最常见的"ArrayList和LinkedList的区别",网上一搜一堆答案,但大多数候选人的回答方式都是:"ArrayList底层是数组,LinkedList底层是双向链表,所以ArrayList查询快、增删慢,LinkedList增删快、查询慢。"这个回答能拿基础分,但拿不到加分。面试官几乎一定会追问一句:"那LinkedList增删真的比ArrayList快吗?"这一问,能答上来的人立刻少一半。正确答案是: 如果是在中间插入,LinkedList确实不需要移动元素,但需要遍历找到插入位置,时间复杂度还是O(n),而ArrayList虽然要移动元素,但它是内存连续的内存拷贝,在数据量小时甚至更快。 所以"LinkedList增删一定快"本身就是错的。

这类细节就是我在整理时特别标注的"追问点"。你真正要背的,是这些追问点背后的逻辑链,而不是表面结论。

另外,时间分配也很重要。很多人的复习策略是:JVM看一个月,并发看一个月,结果算法题没时间刷,基础题也没过一遍。我的建议是分三档: 第一档是"必拿分" ,包括Java基础语法、集合框架、面向对象、异常、泛型,这些属于热身题,答不上来基本就凉了。 第二档是"决胜档" ,包括JVM内存模型、垃圾回收、并发编程、Spring核心原理,这些才是面试官区分中级和高级的地方。 第三档是"亮点档" ,包括网络协议、数据库索引、分布式理论、算法手撕题,根据目标公司的业务特点选择性准备。这套背诵版的排序逻辑就是按这个三档来分的,我在这篇博文里也按这个思路来展开。

2. 基础语法和面向对象:面试前半小时必须过完的送分题

2.1 封装、继承、多态:必须能讲出一个完整的例子

这部分表面太简单,反而很多人栽跟头。面试官问"什么是多态",你要是只答"一个接口多种实现"或者"父类引用指向子类对象",太空了。 面试官真正想听的是:你能不能在JVM层面解释清楚,这个"父类引用调用子类方法"到底是怎么找到子类方法的。 这就是虚方法表、方法分派的考点。

我建议你背一个标准回答框架,而且一定要配合代码讲。比如:

class Animal {
    public void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("Dog barks");
    }
}

Animal a = new Dog();
a.speak(); // 输出 Dog barks

讲这段代码时,你要能顺嘴说出: a 的静态类型是Animal,实际类型是Dog, a.speak() 在编译期看的是Animal类是否有speak方法,没有就编译报错;运行期JVM通过方法表找到实际对象Dog类里重写后的speak方法。这样一个简单的例子,就把重写、静态类型、动态绑定全部串起来了。

2.2 接口和抽象类的本质区别,别只会背"抽象类可以有构造方法"

面试经典题"接口和抽象类的区别"标准答案有三条:抽象类可以有构造方法,接口不能;抽象类可以有成员变量,接口默认是public static final;一个类只能继承一个抽象类,但可以实现多个接口。这三点能背出来,但很多人在"为什么要有这两种东西"这个问题上卡住。面试官往深问一下:"那你什么时候用抽象类,什么时候用接口?"就露馅了。

我的理解框架是: 抽象类是"is-a"关系,强调的是"你是什么",接口是"has-a"能力,强调的是"你能干什么"。 举个实际的例子,定义一个 Bird 抽象类,里面可以放 age 、 weight 这些共有的状态;再定义一个 Flyable 接口, 飞 是一种能力,鸟可以飞,飞机也可以飞,它们并没有继承关系。如果你让一个 Airplane 类去继承 Bird ,就闹笑话了,但它可以实现 Flyable 。这个例子一讲,面试官会觉得你不仅记住了语法规则,还理解了设计意图。

2.3 equals和hashCode:最常见的连环追问现场

这个考点几乎场场必问,而且面试官特别爱连珠炮: "两个对象equals相等,hashCode一定相等吗?反过来呢?不重写hashCode会有什么后果?" 标准逻辑链是这样的: equals() 相等, hashCode() 必须相等,否则HashMap里就找不到这个对象了;反过来, hashCode() 相等, equals() 不一定相等,这是哈希冲突。然后面试官会追问你"那HashMap找对象的过程详细讲讲"。你看,这一题就直接把集合框架的考点也给引出来了。我在背诵版里专门给这类"连锁考点"标了箭头,方便大家用一道题牵扯出一串知识点去复习。

3. 集合框架:一条主线抓住80%的考点

3.1 HashMap原理:背会的同学和理解的同学的差别就在这几个追问

HashMap是Java面试的"题眼",几乎100%会考,而且考得非常深。我从面试官视角不建议你按源码从上到下背,而是建议按"数据从put进去到get出来,中间经历了什么"这条线来串。你可以按下面这个节奏自测一下:

  1. put(key, value) 时,先对key的hashCode做一次扰动计算,也就是 (h = key.hashCode()) ^ (h >>> 16) ,然后根据 (n - 1) & hash 找到桶的位置。
  2. 如果桶为空,直接放Node节点。
  3. 如果有冲突,用链表挂后面,链表长度超过8且数组长度超过64时,转红黑树。
  4. 当数组元素个数超过 threshold = capacity * loadFactor ,默认是0.75,就触发扩容,容量翻倍。

这一串答下来,说明你真看过源码。但面试官还会继续追: 扩容时链表和红黑树怎么处理? 这个就细了。链表在扩容时会拆成低位链和高位链,通过 (e.hash & oldCap) == 0 来判断;红黑树在扩容时如果数量小于等于6会退化成链表。这俩细节很多人到这就答不上来了。我的建议是别死背,你把JDK8的源码拆开,把resize方法认认真真读三遍,理解"为什么是oldCap而不是oldCap-1"这个位运算逻辑,自然就记住了。

"为什么HashMap线程不安全"这个问题也要顺便串进来 :JDK7里扩容时头插法可能造成环形链表死循环,JDK8改成尾插法后不会死循环了,但put覆盖、size不准确的问题还在。所以并发环境必须用ConcurrentHashMap。

3.2 ArrayList和LinkedList:别再背错结论了

前面提到过这个案例。我再补充一个面试官常用的夺命连环问: "ArrayList的默认容量是多少?什么时候会扩容?扩容后变成多少?" 答案:默认容量是10,当添加第11个元素时触发扩容,扩容后变1.5倍,也就是15,老数组复制到新数组。还有个细节是 Arrays.asList() 返回的列表底层还是那个数组,不能调用 add 和 remove ,会抛 UnsupportedOperationException 。这个坑我在实际业务代码里见人踩过不只一次——把 asList 的返回值当成普通List用,运行时直接崩了,这类题面试时也经常以"这段代码有什么问题"的形式出场。

3.3 并发容器:CopyOnWriteArrayList和ConcurrentLinkedQueue

高并发场景下,面试官挺喜欢问:"ArrayList线程不安全,那有什么替代方案?"你至少要能说出 CopyOnWriteArrayList 和 Collections.synchronizedList 。然后顺着追问:"CopyOnWriteArrayList的写操作是怎么保证线程安全的?"答案是写时复制:add时会用ReentrantLock加锁,然后复制一个新数组,在副本上添加元素,改引用指向新数组,读操作不加锁直接读。这个方案的优点是读多写少时效率极高,缺点是内存占用大、数据可能短暂不一致。你能把这个"代价"讲出来,比单纯背优点强太多。

4. JVM与内存:从OutOfMemoryError到垃圾回收,面试官的一套组合拳

4.1 运行时数据区,用"你写的一段Java代码是怎么活过来的"来记忆

很多人在JVM这块的死穴是"记不住"。我教一个笨但有效的办法: 把自己当成一个class对象,从加载到运行,一路上会经过哪些区域。 首先你被类加载器加载,元数据放进"方法区"(JDK8里叫元空间);JVM在"堆"上给你分配好一个对象实例;你所属线程执行你里面的方法时,局部变量和中间计算结果放在"虚拟机栈"里,每调用一个方法就压一个栈帧,方法返回就弹栈;如果你是一个正在执行native方法的人,你还得经过"本地方法栈";最后,你为了让其他线程看到你,指令计数器里的行号一直往前走,记录你执行到哪一句字节码。这一套走下来,六个区域的角色你就全记住了。

4.2 OutOfMemoryError:别把所有的内存溢出都叫"堆溢出"

这里必须提热搜词里那个很扎眼的 java: outofmemoryerror: insufficient memory 。很多人在本地跑IDEA里的小程序遇到这个报错,第一反应是"堆内存不够,加 -Xmx ",但十有八九加完还报。为什么?因为JVM不止堆一个内存区域。 报错信息里写 Java heap space 是堆溢出,直接写 insufficient memory 往往是操作系统层面分配不了内存了,可能的原因是:机器物理内存真的不够了,或者进程地址空间被耗尽,也可能是你同时开了太多JVM实例,把机器的内存全吃了。 你本机跑多个服务时尤其容易出现第二种情况,不是某一个服务堆不够,而是所有服务加起来的占用已经超过物理内存,操作系统开始拒绝分配了。

我在整理这份背诵版时,专门把OOM常见的几个区域和对应报错文案列了个表:

报错中的关键词 内存区域 常见姿势
Java heap space 堆 创建了超大对象、内存泄漏导致对象无法回收
GC overhead limit exceeded 堆 GC一直回收不掉,回收时间超过98%,回收的内存却不足2%
Metaspace / PermGen space 方法区/元空间 反射、动态生成类过多,类加载器无法卸载
unable to create new native thread 虚拟机栈/本地方法栈 线程数超过系统上限,最常见于每次请求都新建线程的服务
Direct buffer memory 直接内存 NIO的DirectByteBuffer使用过多又未释放

这张表大家可以打印下来贴在工位上,比看十遍理论都有用。实际排查的时候,第一步永远是 jmap -heap 进程号 先看各个区域的当前占用,不要瞎猜,然后取一份 jmap -dump 下来的堆快照,用MAT或VisualVM看大对象和引用链。

4.3 垃圾回收判断和常用收集器:G1到底好在哪

"判断哪些对象该回收"这个问题,引用计数法和可达性分析算法要能对比着讲。引用计数的致命缺陷是解决不了循环引用,可达性分析靠 GC Roots 往下遍历,能解决这个问题。面试官一般会追问: 哪些对象可以作为GC Roots? 答案包括:虚拟机栈中引用的对象(也就是正在执行的方法里的局部变量)、方法区中类静态属性引用的对象、常量引用的对象、本地方法栈中JNI引用的对象。你能把这些答全,说明真理解过,而不是只背了"GCRoots是什么"这一句话。

收集器部分,重点从 CMS和G1的对比 切入:CMS老年代标记清除,并发收集,低停顿,但会产生内存碎片,而且浮动垃圾只能留到下一次 Full GC ;G1把堆划分为若干Region,在局部做复制回收,不会产生碎片,还能设置 -XX:MaxGCPauseMillis 来软性控制停顿时间。JDK9里CMS被废弃,默认G1,所以现在面试几乎不深挖CMS的细节了,但对"为什么G1能控制停顿时间"这个问题,你得能说清楚:G1会跟踪每个Region的回收价值和耗时,优先回收收益最大的Region,这种思路叫"垃圾优先"。

4.4 类加载机制:双亲委派不是背个名字就结束的

类加载这块的套路我也摸清了:先问"什么是双亲委派",再问"为什么要设计成双亲委派",最后问"怎么打破双亲委派,Tomcat怎么做的"。完整答题链路是:类加载请求先交给父加载器,父加载器处理不了再由子加载器自己加载。 目的有两个:防止核心API库被篡改,比如你自己写一个 java.lang.String ,不会被加载进来顶替JDK的String;另一个是避免类被重复加载。 打破双亲委派的方式是继承ClassLoader类并重写loadClass方法,Tomcat为了给每个Web应用提供独立的类库隔离,用WebAppClassLoader优先加载Web应用自己的类,找不到再交给父加载器。能讲到这个层面,面试官就会觉得你不仅背了,还研究过。

5. 并发编程:从关键字到线程池,每一环都是夺命题

5.1 synchronized和ReentrantLock的对比,答得好能直接镇住全场

这题是并发部分最值得花时间准备的,没有之一。我的答题框架分五层,从浅到深可以自己评估:

  • 第一层 :语法层面,synchronized是关键字,自动加锁释放锁;ReentrantLock是API,需要手动lock和unlock。
  • 第二层 :锁的灵活度,ReentrantLock支持公平锁和非公平锁,synchronized只有非公平锁。
  • 第三层 :中断响应,ReentrantLock的 lockInterruptibly() 能响应线程中断,synchronized不行。
  • 第四层 :条件等待,ReentrantLock可以创建多个Condition,精准唤醒特定线程;synchronized只能配合wait/notify,多个等待条件时处理起来很别扭。
  • 第五层 :底层实现,synchronized经过锁升级过程,无锁→偏向锁→轻量级锁→重量级锁,ReentrantLock底层用CAS+AQS实现。

能讲到第五层,面试官基本会打高分。尤其现在JDK对synchronized做了大量优化,锁升级的过程本身就是一个高频追问点。你可以记住一个生活化类比: 锁升级就像一个人滑冰——先穿上冰刀(无锁/CAS),滑得顺就不换鞋;滑不起来了,先换成轮滑鞋(轻量级锁,自旋);还是不行,最后才穿上最笨重但最稳的滑雪靴(重量级锁,操作系统互斥量)。

5.2 volatile:为什么它能保证可见性,却保证不了原子性

volatile必考三板斧:可见性、有序性、不保证原子性。 可见性怎么实现? 线程修改volatile变量后,会强制把工作内存里的最新值刷回主内存;读volatile变量时,会强制从主内存读取,而不是用线程本地缓存。 有序性怎么体现? volatile变量读写会插入内存屏障,禁止指令重排,著名的单例双重检查锁就必须依赖volatile。 为什么保证不了原子性? 因为 count++ 这种操作在字节码层面是四条指令:读、修改、写回,volatile只保证每次读都读到最新值,但两个线程可能同时读到同一个旧值然后各自加一,丢更新。这个"读改写三步为什么会有问题"的底层逻辑要自己讲一遍,光背结论容易在追问中露馅。

5.3 线程池:七个参数+拒绝策略+为什么不能用Executors

线程池的提问套路也非常固定。先问七大参数,这里要特别小心: 阻塞队列的选择、饱和策略的选择,是面试官最爱展开追问的地方。 然后会问"如果核心线程都在忙,队列也满了,新任务来了怎么办",这就是各种拒绝策略的适用场景: AbortPolicy 抛异常、 CallerRunsPolicy 调用者线程自己跑、 DiscardPolicy 静默丢弃、 DiscardOldestPolicy 丢弃最老任务。我建议把四种策略背熟之后,一定要和实际场景挂钩想一遍: 比如你是做接口调用的,肯定不能选DiscardPolicy,不然请求悄悄丢了,线上问题都没法排查;一般建议自定义ThreadFactory并且把拒绝策略写成打日志+降级处理。 最后面试官会问"为什么《阿里巴巴Java开发手册》不让用Executors":因为 Executors.newFixedThreadPool() 默认的队列是 LinkedBlockingQueue ,队列长度无限大,瞬间来大量任务时内存直接爆掉; newCachedThreadPool() 最大线程数是 Integer.MAX_VALUE ,线程数创建过多会导致OOM。这两个坑我都见过真实生产事故,所以老实自定义线程池参数,永远别偷懒。

6. 手撕代码与算法:排序是八股的变体,但不是死背模板

6.1 冒泡排序和快速排序的高频手撕场景

面试手撕环节,排序算法出现的概率极高。 冒泡排序是入门级,但面试官考的不只是写出来,而是让你优化。 纯正的冒泡是双重循环,最外层跑n-1趟,内层每趟比较相邻两数,逆序就交换。但这种写法在数组本身有序时,依然要比较 n(n-1)/2 次,时间复杂度O(n²),浪费严重。优化点是加一个标志位 swapped :如果某一趟遍历下来没有任何交换,说明整个数组已经有序,直接break。优化后最好情况时间复杂度降到O(n)。这个优化点,你要能在两分钟之内写出来:

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;
    }
}

快速排序是另一个高频题,也是最容易写错的一个。 核心逻辑是:选一个基准值(pivot),把比它小的放左边,比它大的放右边,再对左右递归。我见过大量候选人思路完全正确,但递归边界处理不好,要么死循环要么数组越界,原因就在于递归终止条件写错。给你一个我背熟且几乎不出bug的模板:

public void quickSort(int[] arr, int left, int right) {
    if (left >= right) return; // 终止条件,一写错就栈溢出
    int pivot = arr[left];
    int i = left;
    int j = right;
    while (i < j) {
        // 从右往左找第一个小于pivot的数
        while (i < j && arr[j] >= pivot) j--;
        // 从左往右找第一个大于pivot的数
        while (i < j && arr[i] <= pivot) i++;
        if (i < j) {
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
    }
    // 把基准值放到正确位置
    arr[left] = arr[i];
    arr[i] = pivot;
    quickSort(arr, left, i - 1);
    quickSort(arr, i + 1, right);
}

这个写法我是真在面试白板上写过很多次,稳定不出错。注意看两个内层while必须带 i < j 的判断,不然边界位置就乱了。

6.2 二分查找:边界条件是分水岭,左右闭区间和左闭右开别混着写

二分查找在Java面试中也特别常考,而且 越简单越容易写出bug 。最常见的错误是 mid = (left + right) / 2 在极端情况下整数溢出,正确写法是 mid = left + (right - left) / 2 。然后是边界条件,我个人建议平时只练一种写法并且形成肌肉记忆:用 左闭右闭区间 ,也就是 [left, right] ,循环条件是 while (left <= right) ,判断后更新时 left = mid + 1 或 right = mid - 1 。这套写法逻辑自洽,不容易记混。我把两种写法的对照也放一下,方便大家看清楚区别在哪:

写法 初始区间 循环条件 更新方式 防止死循环的要点
左闭右闭 [0, n-1] left <= right left = mid + 1 / right = mid - 1 区间始终有效
左闭右开 [0, n) left < right left = mid + 1 / right = mid right = mid 不能减一

6.3 还有两个必须秒答的小题:数组越界和值传递

热搜词里出现了"java中数组越界异常"和"java标识符命名规则"这两类基础,跟手撕代码不直接相关,但我强烈建议你花十分钟过一遍。 数组越界 要能说出异常类名是 ArrayIndexOutOfBoundsException ,它是 IndexOutOfBoundsException 的子类,而 IndexOutOfBoundsException 是 RuntimeException 的子类,所以编译器不会强制你捕获,但运行时崩了非常难排查,因为堆栈信息往往只告诉你"越界了",不告诉你哪个数组。 值传递和引用传递 这个话题在Java里特别容易吵,核心就一句: Java只有值传递 。谁再说"对象是引用传递",你可以直接反驳他——传引用时传的其实是引用变量的值拷贝,也就是说,方法里改引用本身,方法外完全不受影响;但方法里通过这个引用去修改对象内部状态,方法外看得到。改了引用等于把"通讯录卡片上的地址"擦了重写,原卡片当然不受影响;而修改地址指向的那个人就是修改对象本身,自然两边都看到变化。用这个例子,一下就通了。

7. Java 8与编程细节:Lambda、枚举、聚合操作的实际体验

7.1 Lambda表达式:面试官会问"它到底是什么语法糖"

Lambda在热搜词里出现了,面试考得不算难,但会往"底层"问一下。 标准问题:Lambda表达式的本质是什么? 答案:函数式接口的匿名实现类的语法糖。 函数式接口就是只含一个抽象方法的接口,比如 Runnable 、 Comparator 、 Function 这些。 你要能随口说出来Lambda表达式的完整写法、简化写法、能捕获哪些变量—— final 或"事实final"的局部变量。我用得最多的一个场景是Comparator链式排序,这在处理列表时特别香:

List<User> users = getUsers();
users.sort(Comparator.comparing(User::getAge)
        .thenComparing(User::getName));

这一段比我写一堆for循环加匿名内部类Comparator清爽太多了。面试官如果要你对比"Lambda和匿名内部类有什么区别",你至少要能说出来:Lambda不会额外生成class文件(方法运行时通过 invokedynamic 指令生成),而匿名内部类会在编译期生成独立的class文件。

7.2 枚举类型:不只是"用来表示常量"

Java枚举这个考点看着简单,其实展开讲可以很深。 定义常量用枚举,和用 public static final int 有什么区别? 标准答案:枚举是类型安全的,传入一个不存在的枚举值编译器直接报错;而int常量你传个任意int进去,编译期完全检测不出来。比如你定义一个 Status 枚举,有 SUCCESS 和 FAIL ,在方法参数里写 Status status ,别人没法传一个 3 进来,但你要用int常量定义,我传 3 你也只能运行时发现不对。另外, 枚举还可以带字段和构造方法,还能定义抽象方法由每个实例分别实现 ,这在实现策略模式时就很好用。我整理过一个简单实例:

public enum OrderStatus {
    CREATED(1) {
        @Override
        public boolean canCancel() { return true; }
    },
    PAID(2) {
        @Override
        public boolean canCancel() { return false; }
    };
    private final int code;
    OrderStatus(int code) { this.code = code; }
    public int getCode() { return code; }
    public abstract boolean canCancel();
}

把状态和它对应的行为放在同一个枚举里,消除了一大堆if-else,代码干净不少,面试时讲出来也是加分项。

7.3 聚合操作与Stream API:别光背名词,得说出你用过的场景

"java聚合"这个热搜词,对应的就是Java 8里的 Stream 聚合操作。面试官问"你用过Stream吗"绝对不是让你说"用过"就完了,他要你说出几个具体的聚合操作以及它们的执行逻辑。我在实际代码里最常用的有几个: filter 过滤、 map 转换、 collect(Collectors.toList()) 收集、 groupingBy 分组、 reduce 归约。举个例子,有一批订单,要统计每个用户的总金额:

Map<String, BigDecimal> totalByUser = orders.stream()
        .filter(o -> o.getStatus() != OrderStatus.CANCELLED)
        .collect(Collectors.groupingBy(Order::getUserId,
                Collectors.mapping(Order::getAmount,
                        Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));

当你真能在白板上写出这样一行,面试官就知道你平时在真实项目中是认真用过Stream的。还有几个容易被追问的坑要提前踩平: stream() 和 parallelStream() 的区别(并行流用的是ForkJoinPool公共线程池,用不好会互相干扰)、 peek 和 map 的区别(peek是中间操作,用于调试,不会改变元素本身;map是转换,会返回新元素)。

8. 工具链与工程环境:排查源发行版17、乱码和本地方法找不到的实战备忘

8.1 源发行版 17 需要目标发行版 17 :IDEA编译报错的终极解法

热搜词里出现了 java: 警告: 源发行版 17 需要目标发行版 17 ,这是被问爆的IDEA编译问题。 出现这个警告的根本原因是:项目编译用的JDK版本和实际运行的JDK版本不一致,或者Maven的 source/target 版本配置和当前JDK版本不匹配。 比如你本机装的是JDK8,但项目 pom.xml 里设了:

<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

Maven在JDK8上跑就会报这个警告。解决办法有两个方向:一是把 pom.xml 的版本降成和本机JDK一致;二是在IDEA里把所有JVM相关的设置统一,包括 File -> Project Structure 里的Project SDK、Project language level、Modules的language level,还有 Settings -> Build Tools -> Maven -> Importing 里的JDK for importer。 很多人改了pom但还是报错,就是因为IDEA里Project Structure的language level还停在旧版本 ,IDEA的language level是独立于pom配置的,两处必须同时对齐。我踩过这个坑后,形成了一套固定动作:pom的source/target、Project SDK、Project language level、Modules language level、Maven JDK,五个地方全部检查一遍,不会漏。

8.2 VSCode运行Java报错乱码:从控制台编码到文件编码的排查链路

乱码问题在Java开发里太常见了,尤其在VSCode里跑Java,报错信息全是乱码,简直心态爆炸。 根源一般有两种:控制台输出编码和源码文件编码不一致,或者编译期编码和运行期编码不一致。 Java源代码文件默认用UTF-8保存,但Windows控制台默认可能是GBK,System.out输出UTF-8的内容给GBK控制台打印,自然乱码。解决的命令行参数是 -Dfile.encoding=UTF-8 ,在VSCode的 launch.json 里加上:

"vmArgs": "-Dfile.encoding=UTF-8"

同时把VSCode的终端编码也调成UTF-8。还有一类更隐蔽的情况:你源码文件本身是GBK编码保存的,但Maven编译时强制按UTF-8编译,这种直接编译报错或者输出乱码,需要在 pom.xml 里通过 project.build.sourceEncoding 统一指定。排查乱码的思路其实只有一条: 先确定源码是什么编码、编译时按什么编码、运行时输出用什么编码解码,三处统一为UTF-8就解决了90%的乱码问题。

8.3 Lombok报错: You aren't using a compiler supported by lombok

热搜词里还出现了一个挺坑的报错: java: You aren't using a compiler supported by lombok, so lombok will not work 。这个报错我见过很多人一脸懵。 原因通常是你用的IDEA版本太新,IDEA自带的编译器版本超出了Lombok当前版本已知支持的编译器列表。 解决办法按优先级分别是:更新Lombok依赖到最新版本;如果你项目里指定了 annotationProcessorPaths ,确认Lombok在路径里且版本一致;在IDEA的 Settings -> Build -> Compiler 里把编译方式改成 javac 而不是用内部的编译器。另外还要确认IDEA里安装了Lombok插件且 Enable annotation processing 勾选了,这个勾选在 Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors 里。三个地方检查完,基本都能解决。

8.4 环境变量配置:JAVA_HOME和PATH的"为什么"比"怎么配"更值钱

最后一个和环境相关的经典问题:Java环境变量配置。大多数人只会照着教程配JAVA_HOME和PATH,但面试官会问"为什么要配JAVA_HOME"—— 因为这个环境变量是很多中间件找JDK的唯一依据 ,比如Tomcat、Maven、Idea的启动脚本都靠 JAVA_HOME 来定位Java安装目录,你只配PATH不配JAVA_HOME,命令行能用java,但Tomcat直接起不来。另外还有 classpath 的概念,以前的JDK需要手动配置 .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar ,JDK1.5之后不再必须了,但面试官偶尔会问,你只要说出来"JDK1.5之后默认会从当前目录和rt.jar加载类,所以不需要手动配置classpath",这个回答就很干脆。

9. 最后再分享一点我个人的复习心得

整理这份背诵版的过程中,我最大的感受是: 真正有用的八股文整理,不是把知识点堆在一起,而是把知识点之间的"连线"画出来。 比如你背了HashMap的原理,就一定顺带把 hashCode和equals 、 红黑树 、 线程安全问题 、 ConcurrentHashMap 全都串一遍,这种"由一个点带出一张网"的复习法,远比按章节从头到尾翻书高效。我在面试别人时也特别能感受到:一个人如果能把两三个知识点自然串联起来讲,他的知识体系一定不是临时背出来的。另外,强烈建议你对着镜子或者录个音,把每个高频问题用口述的方式讲一遍,因为在心里默读懂和开口讲出来完全是两码事,语速、停顿、逻辑条理都需要练习。祝所有正在准备面试的朋友都能拿到心仪的offer。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐