2026年Java面试高频考点白话解析,助你搞定春招秋招

一转眼又到了新一轮招聘季。最近后台收到最多的私信就是:“2026年Java面试题有没有最新整理?最好带白话答案。”说实话,Java面试题在网上从来不缺,缺的是那种能把原理讲明白、还能让人真正听懂的答案。我这些年待过几家不同类型的公司,也当过多轮面试官,前前后后整理了好几版面试题清单。今天这篇就把适合2026年节奏的高频考点重新梳一遍,每条尽量用大白话把底层逻辑讲透。内容不追求堆题量,而是按模块把每一类题的考点、追问方向、答题套路拆开,适合准备春招秋招的同学,也适合工作两三年的Java开发用来查漏补缺。

1. 2026年Java面试到底考什么:先看清风向再背题

1.1 从“背八股文”到“场景+原理追问”

很多准备面试的同学还停留在背题阶段,打开收藏夹就是几千道题的题库,背完HashMap又背JVM参数,结果一到现场就露馅。我当面试官这几年最深的感觉是:现在的面试,尤其是稍微好一点的公司,已经不怎么吃“八股文”那一套了。

面试官问HashMap,不会只问你“底层是什么结构”,而是会追问“为什么链表长度到8才转红黑树”“扩容为什么是2的幂次方”“JDK7和JDK8的并发问题有什么区别”。问Spring Boot,不会只问“自动装配是什么”,而是会直接把问题抛到场景里:“公司有一个老项目依赖冲突,你怎么定位”或者“如果你要写一个自己的starter,核心步骤是什么”。

这背后的逻辑很简单:背答案只能证明你看了面经,而追问才能考查你是否真的理解。2026年的面试风向会更加明显——考点本身可能还是那些,但提问方式会越来越像一次小型的系统设计讨论。所以这篇文章里的每一道题,我除了给答案,还会点出面试官真正想听的内容和常见的追问方向。

1.2 这套题单怎么用:先搭框架再抠细节

我整理这套题单的时候,特意把内容分成Java基础、并发与线程、JVM、Spring与Spring Boot、分布式与中间件这几个大块。这几个模块覆盖了目前主流Java岗位面试里大概80%以上的考点,尤其是以业务开发为主的岗位,基本不会跳出这个大框架。

使用方式建议是:先看每个章节最前面总结的“核心考点”,闭卷想一遍自己能答出多少,再对照后面的白话答案去补。不要一上来就逐条背,而是把每道题当成一个知识的锚点——比如HashMap那道题展开后能串起哈希函数、扰动算法、扩容机制、红黑树、fail-fast,这些知识点其实是环环相扣的。按模块去理解,比孤立背题高效得多。

2. Java基础高频题:String、集合与HashMap底层白话拆解

2.1 String、StringBuilder、StringBuffer:一道题能问出三层水平

String为什么不可变,这道题几乎是必考的。白话答案分三层讲清楚:第一,String类本身被final修饰,不可继承;第二,内部存储用的char数组或者byte数组也被final和private修饰,外部拿不到引用;第三,String类没有暴露任何可以修改内部字符数组的方法,所有看起来像修改的操作,比如substring、replace、concat,实际上都返回了一个新对象。

面试官紧接着会问“不可变有什么好处”,这就要往应用层靠了。首先是线程安全,因为内容不会变,多线程环境下不需要加锁就能安全共享;其次是hashCode可以缓存,String常被用作HashMap的key,第一次计算完哈希值后直接缓存,不用每次重算;再就是字符串常量池的复用,如果字符串可变,常量池里的值就可能被一个引用修改掉,导致其他引用拿到的值也变了,这会让整个系统乱套。

StringBuilder和StringBuffer的区别更简单,一句话:StringBuffer的方法加了synchronized,所以线程安全但性能稍低,StringBuilder没有加锁,性能更好但线程不安全。但要小心,面试官问到这里往往会有坑——很多教材会说“StringBuffer是线程安全的,所以并发场景要用StringBuffer”,这个说法在Java面试里已经过时了。实际上局部变量根本不存在线程安全问题,而全局共享的字符串拼接也几乎不会用StringBuffer去解决,更常见的做法是用线程安全的容器或者干脆避免共享可变字符串。面试官想听到的答案是你知道这个区别,同时还能补充一句“线程安全不代表就该用它”。

2.2 HashMap的put流程、扩容机制和并发问题

HashMap是Java面试里当之无愧的题王,几乎每家必问。白话版本可以这样组织:

底层结构是数组加链表,JDK8之后链表长度超过8且数组长度大于等于64时,会转为红黑树。put一个key-value时,流程是这样的:先对key做hash,JDK8的hash函数是高位异或低位,也就是h = key.hashCode() ^ (h >>> 16),这么做的目的是让高位也参与后续的数组下标计算,减少哈希碰撞。然后用hash值和数组长度减一做与运算,即hash & (n-1),得到存放位置。为什么用与运算而不用取模?因为当数组长度是2的幂次方时,hash & (n-1)和hash % n的结果完全一样,但位运算更快。

如果这个位置是空的,直接放入新节点;如果已经有节点了,就遍历链表或者树。如果链表里已经有相同key,就覆盖旧值;如果没有相同key,就挂在链表尾部。插入完成后检查当前元素个数是否超过threshold,也就是容量乘以加载因子0.75的积,如果超过就进行扩容,容量翻倍。

扩容是另一个高频考点。为什么必须是2的幂次方,不只是因为位运算快,还因为扩容后元素重新分布有个很好的性质:元素在新数组中的位置要么不变,要么就是原索引加上旧容量。这个结论直接用hash & (n-1)就能推出来,因为扩容后n的最高位多了一个1,相当于把原来的索引最高位再判断一次。JDK8里甚至针对这个性质做了优化,不需要每个节点重新计算hash,只需要看原来的hash值在新增的那一位上是0还是1,是0就留在原地,是1就移动到原索引加旧容量的位置。

面试官特别喜欢追问JDK7和JDK8并发问题的区别。JDK7用的是头插法,扩容时在多线程场景下可能出现循环链表,get的时候会死循环;JDK8改成了尾插法,循环链表的问题解决了,但并发下仍然不安全,常见的表现是丢数据——两个线程同时put到同一个空桶时,都发现是空位,都直接插入,后写的覆盖先写的;另外size这个字段不是原子的,多线程下统计的size不准确。这题的标准答案是:HashMap线程不安全,并发场景请用ConcurrentHashMap。

注意:面试时尽量不要只说结论。把上面的时间点、数据结构变化、并发问题表现串起来讲,会让面试官觉得你是真读过源码,而不是背了面经。

2.3 ArrayList和LinkedList:别再只答“数组与链表”

ArrayList和LinkedList的区别也是基础题里的常客,但很多人的答案只有一句话“ArrayList是数组实现,LinkedList是链表实现”。这句话没错,但远不够。

补充几个关键差异:ArrayList随机访问是O(1),LinkedList随机访问是O(n),要一个个遍历;ArrayList在尾部添加元素平均是O(1),扩容时是O(n),LinkedList在头部插入效率高,是O(1),ArrayList在头部插入需要搬移所有元素,是O(n)。ArrayList扩容是每次增加约1.5倍,通过位运算算出来的,本质是int newCapacity = oldCapacity + (oldCapacity >> 1)。

面试官经常在这后面设置几个细节坑:第一个是Arrays.asList()返回的List能不能add,答案是会抛UnsupportedOperationException,因为这个List是Arrays内部类,底层还是数组,没有实现add和remove方法。第二个是ArrayList的subList返回的是视图,不是复制,修改子列表会影响原列表,而且原列表结构发生修改后,子列表会抛出ConcurrentModificationException。第三个是ArrayList的toArray()方法,返回的Object[]不能强转为String[],数组强转会抛ClassCastException,要传一个指定类型的空数组,比如list.toArray(new String[0])。

3. 并发与线程:面试官最爱追问的volatile、ThreadLocal和线程等待

3.1 volatile只保证可见性,不保证原子性,这道题错的人最多

并发编程是Java面试里区分度最高的部分,而volatile几乎是被误解最多的一道题。白话解释是这样的:Java内存模型规定,每个线程都有自己的工作内存,线程读写变量时先操作工作内存,再同步回主内存。如果没有同步机制,一个线程改了变量,另一个线程可能长时间看不到。volatile做的事情有两件:一是每次读写都直接操作主内存,让修改对其他线程立刻可见;二是禁止指令重排序,保证某些代码顺序不会被编译器或CPU打乱。

但volatile不保证原子性。经典的反例就是count++,这个操作看起来是一行代码,但底部分成三步:读取count的值、加1、写回。两个线程同时读取到相同值,各自加1再写回,结果只加了1,这就是丢更新。所以volatile适合什么场景呢?适合一个线程写、多个线程读的开关型状态变量,比如一个boolean类型的停止标记。如果想保证原子操作,就要用synchronized或者AtomicInteger这类原子类。

这道题还有一个很自然的追问方向:synchronized和volatile的区别。答案可以从三个方面说:volatile是轻量级的,功能上只能保证可见性和有序性,synchronized既能保证原子性也能保证可见性;volatile不能修饰方法,也不能用在代码块里;volatile不会阻塞线程,synchronized在竞争激烈时会阻塞和唤醒线程。

3.2 ThreadLocal的内存泄漏问题:为什么每次用完要remove

ThreadLocal在面试里也是常客,尤其现在很多框架都用它来传递上下文,比如Spring的事务管理、日志链路追踪里的traceId。关于ThreadLocal的内存泄漏,几乎每个面试官都会问到。

先讲原理:每个Thread内部维护了一个ThreadLocalMap,这个Map的key是ThreadLocal对象本身,value是你set进去的值。关键问题出在key是WeakReference弱引用,而value是强引用。当外部没有强引用指向ThreadLocal对象时,这个key就会被垃圾回收掉,但value还留在ThreadLocalMap里。如果这个线程一直存活,比如线程池里的核心线程,Value就永远无法被回收,慢慢就把内存堆满了。

所以答案里一定要包含“适用完调用remove()释放”这一条。在项目里,如果使用线程池并且在线程里set了ThreadLocal,务必在finally代码块中调用remove。这是标准答案,也是实际开发中必须养成的习惯。

顺便提一个加分点:ThreadLocal在Spring框架里也有使用场景,但要注意父子线程之间ThreadLocal是无法共享的。如果面试官问“怎么让子线程拿到父线程的ThreadLocal值”,可以提一下InheritableThreadLocal,但要注意线程池场景下它依然不靠谱,更现代的做法是用阿里的TransmittableThreadLocal,专门解决线程池中上下文传递问题。这一句虽然简单,但会让面试官觉得你真有实战经验。

3.3 等所有线程都执行完成:CountDownLatch、CyclicBarrier和Future

“java线程等待都完成”这个搜索词来得很直接,它就是一道并发场景题:主线程要等待多个子线程全部执行完,再统一做下一步处理,应该怎么做。最老的答案是用Thread.join(),主线程调thread.join()会阻塞等待对应线程终止,多个线程就依次join。这个方案能跑,但不够优雅,而且没法控制等待超时。

现代一点的标准答案是CountDownLatch,白话解释就是设置一个计数器,每个子线程执行完调用一次countDown(),把计数减一,主线程调用await()阻塞等待,直到计数器归零。注意countDownLatch是一次性的,计数到0之后就不能重用了。

如果存在多个线程互相等待、到达某个屏障后再一起继续执行的场景,应该用CyclicBarrier,它的特点是可循环使用。区别记忆法:CountDownLatch是“主线程等子线程”,CyclicBarrier是“子线程互相等,凑齐一波一起出发”。

还有一个高频写法是使用ExecutorService配合Future。提交任务后拿到Future对象,调用future.get()会阻塞直到对应任务完成。如果提交一个任务列表,可以用invokeAll(),这个方法会等待所有任务完成,然后返回所有Future结果。这套方案的优势是能拿到每个任务的计算结果,而且可以配合Future的get(timeout)设置超时,防止某个任务卡死导致整个流程永久等待。

实操提醒:如果你在线上项目里遇到这种需求,优先用线程池提交任务加invokeAll,或者用CountDownLatch,别自己在代码里new一堆Thread出来。不可控的裸线程是生产事故的高发源头。

4. JVM必考点:内存结构、垃圾回收与类加载的白话解释

4.1 JVM内存结构:别再和Java内存模型混为一谈

JVM相关的面试题里,最容易出状况的其实是概念混淆。先明确两个概念:JVM内存结构指的是运行时数据区,它分为堆、虚拟机栈、本地方法栈、方法区、程序计数器;Java内存模型则是JMM,它规定的是抽象内存模型,主内存和工作内存。两个东西名字像,但完全是两码事,面试时如果被问JMM,别把“堆栈”背上去。

白话版堆与栈讲解:堆是存放对象实例的区域,所有new出来的对象都在堆上分配,堆是垃圾回收的主战场,又分年轻代和老年代,年轻代里还分Eden区和两个Survivor区。虚拟机栈是每个线程私有的,每执行一个方法就会创建一个栈帧,栈帧里存放局部变量表、操作数栈、方法返回地址等数据。和方法相关的局部变量,比如基本类型和对象引用,存在栈帧里;对象本身还是存在堆里。方法区在JDK8之后被移到了元空间,存放类信息、常量、静态变量等,元空间使用本地内存,不再受堆大小的限制。程序计数器用来记录当前线程执行的字节码行号,也是线程私有的。

面试官经常问一个很切口的问题:“对象一定分配在堆上吗?”这是个加分点。答案是不一定,在某些情况下JIT编译器会做逃逸分析,如果对象不会被外部引用,也就是没有逃逸出方法作用域,可能被分配到栈上,方法执行完直接弹栈,连GC都不用参与。虽然这个优化在目前的JVM实现里还不够普遍,但能答出来说明你真看过虚拟机细节。

4.2 垃圾回收算法和垃圾收集器选择

垃圾回收的题目很固定:先判断哪些对象是垃圾,再讨论怎么回收。判断垃圾的标准是可达性分析,从GC Roots出发,一路遍历引用链,能到达的对象就是活着的,不能被回收的,到不了的就是垃圾。GC Roots包括栈帧中的局部变量、静态变量、常量引用的对象、本地方法栈引用的JNI对象等。

回收算法有三个大方向:标记-清除、标记-复制、标记-整理。标记-清除效率高但会产生内存碎片;标记-复制把内存分成两块,每次只用一块,回收时把存活对象复制到另一块,适合年轻代的“朝生夕死”特性;标记-整理是把存活对象往一端移动,解决内存碎片问题,适合老年代。

接下来就是收集器的选择,近几年面试重点已经从CMS转向G1,甚至ZGC也开始出现。记忆要点是:CMS是第一款真正意义上的并发收集器,目标是低停顿,但它的缺点是会产生内存碎片,而且并发阶段和业务线程抢CPU,在某些JDK版本里还可能因为“Concurrent Mode Failure”退化成Serial Old串行回收,停顿时间更长。G1则是把堆分成一个个Region区,维护一个可预测的停顿时间模型,通过软实时的方式尽量控制在用户指定的停顿时间内,JDK9之后成为默认收集器。如果问到ZGC,知道它是超低停顿、基于Region、使用染色指针和读屏障技术、停顿时间在几毫秒级别的下一代收集器,就足够了,中小厂面试基本不会深挖ZGC内部实现。

注意:回答垃圾收集器时,一定不能只说“CMS好还是G1好”这种话。要说清楚“选型取决于业务场景”,比如堆内存几十个G、要求低停顿,优先G1;如果是小堆、单机应用,用默认或直接选串行都行。面试官想听的是权衡思维。

4.3 双亲委派模型:Java类加载的经典机制

类加载这块的核心是双亲委派。白话解释很简单:某个类加载器收到类加载请求时,不会自己先去加载,而是先把请求交给父加载器,一层层往上委托,直到最顶层的启动类加载器,如果父加载器加载不了,才会往下回退,让子加载器自己尝试加载。

为什么这样设计?核心原因有两条:第一是避免类被重复加载,统一由父加载器加载,可以保证同一个类只会被加载一次;第二是保证核心Java类库的安全,比如你自己定义一个java.lang.String,如果系统先加载了你写的String,那整个Java世界就乱了,双亲委派机制保证核心API一定是被启动类加载器加载,想用自定义类去冒充核心类也行不通。

追问方向一般是“怎么打破双亲委派”。这个问题提一下就够,能说出来Tomcat就是典型的打破案例,因为每个Web应用要互相隔离类,所以它自己实现了一个WebAppClassLoader,先加载Web应用自己的类,再委派给父加载器,这就打破了向上委派的顺序。

5. Spring与Spring Boot:从IoC、AOP到自动装配的完整链路

5.1 用生活例子讲清楚IoC和AOP

Spring题目的核心是IoC和AOP。IoC全称是控制反转,很多人解释得云里雾里。换个方式说:以前的代码是你自己new对象,然后自己管理对象之间的依赖关系;用了Spring之后,对象的创建和维护交给Spring容器,你只需要声明“我要用什么”,容器就会把相应的实例注入进来,这就是依赖注入(DI)的应用方式。

白话类比:像去餐厅吃饭,你不用知道后厨怎么买菜、掌勺师傅是谁,只需要跟服务员说自己要什么菜,菜就会端上来。Spring容器就是这个服务员,它帮你把各种对象组装好,你只管点菜和吃菜。IoC的好处是解耦,代码里不再出现一长串的new,想替换实现类时也只需要改配置或加注解。

AOP的英文是面向切面编程,意思是把一些与业务逻辑无关的通用逻辑,比如日志记录、权限校验、事务控制,从业务方法里剥离出来,通过切面统一织入。支撑AOP的技术是动态代理——Spring在运行时为你的对象生成一个代理对象,外部调用的其实是代理对象,代理对象先执行切面逻辑,再调用真实的业务方法。

追问肯定是“JDK动态代理和CGLIB有什么区别”。答案很固定:JDK动态代理基于接口,通过Proxy和InvocationHandler在运行时为接口生成代理类,所以目标类必须实现接口;CGLIB通过继承目标类生成子类来创建代理,不需要接口,但目标类不能被final修饰。Spring Boot 2.x默认使用的是CGLIB方式,因为它在不需要接口的场景下更通用。

5.2 Spring循环依赖:为什么“三级缓存”能解决

循环依赖指的是A依赖B,B又依赖A。Spring能通过三级缓存解决构造函数注入之外的循环依赖,这题已经变成Spring面试的标配了。

先记三个缓存的职责:一级缓存存放已经创建完成的完整Bean;二级缓存存放已经实例化但还没完成属性填充的早期Bean,如果有AOP代理,这里放的是代理对象;三级缓存存放的是一个ObjectFactory,一个函数式工厂,等真正需要注入时,调用factory.getObject()来获取提前曝光的对象。

整个流程白话讲:创建A时,发现A依赖B,就去创建B;创建B时,发现B依赖A,此时A虽然还没完成属性填充,但已经把ObjectFactory放进了三级缓存,B就从三级缓存中拿到A的早期引用,完成自己的创建;B创建完成后,再回到A的创建流程,把B注入进去,A也创建完成。

为什么需要三级而不是二级?关键在于和AOP代理的结合。Spring希望在实例化早期就暴露原始对象,但如果有切面,代理对象需要等切面信息准备好才能创建。如果只有二级缓存,那么每次从缓存取出的都可能是不带代理的原始对象。所以Spring用三级缓存,先存一个ObjectFactory,保证A发生循环依赖时,能从工厂里拿到一个正确的、可能已经包含AOP代理的A。

5.3 Spring Boot自动装配:面试新宠的底层逻辑

Spring Boot最让人惊艳的就是自动装配,一问到原理就得往注解上拆。@SpringBootApplication实际上由三个注解组成:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。

核心在@EnableAutoConfiguration,它通过AutoConfigurationImportSelector,扫描所有META-INF目录下的spring.factories或AutoConfiguration.imports文件,读取里面的自动配置类列表,再结合@ConditionalOnXxx系列条件注解,按需加载。比如Classpath里有Tomcat且没有自定义的嵌入式服务器配置,就自动装配Tomcat;classpath里有DataSource,就自动配置数据源和JdbcTemplate。

面试官喜欢追问“条件注解的工作原理”,你可以这样答:@ConditionalOnClass会检查Classpath中是否存在指定的类,@ConditionalOnMissingBean会检查当前容器是否有某个Bean,这些条件判断都是通过Condition接口实现的,Spring在注册BeanDefinition时先执行匹配逻辑,匹配通过才注册。再补一句实战经验:如果你想写自己的starter,核心工作就是两个,一个编写自动配置类,一个在spring.factories或AutoConfiguration.imports里注册这个类。

6. Redis、MySQL与分布式:中间件面试题实战拆解

6.1 Redis数据结构与缓存“三兄弟”:穿透、击穿、雪崩

Redis面试题的核心考点之一是“你说说Redis的五种基本数据结构”,但只会报名字没什么用,要说使用场景。String是最简单的KV结构,常用做计数器和缓存对象;List常用于消息队列、最新消息列表这类需要按顺序读写的场景;Hash适合存储对象,比如用户信息,能单独操作某个字段;Set适合去重、抽奖、共同好友这种集合运算;ZSet因为带分数排序,适合排行榜、延迟队列。

更高频的是缓存三兄弟,也就是缓存穿透、缓存击穿、缓存雪崩。缓存穿透是指查询一个根本不存在的数据,缓存和数据库里都没有,请求会直接打到数据库上去。解决思路是对空结果也做缓存,或者用布隆过滤器先把不可能存在的key过滤掉;缓存击穿是指某个热点key到了过期时间,一瞬间大量请求打到数据库。解决思路是热点数据设置永不过期,或者访问数据库时用互斥锁,只有一个线程去查库,其他线程等待;缓存雪崩是指大量key同时过期,请求集中打到数据库。解决思路是过期时间加随机值,避免同一时间集体失效,再叠加多级缓存和限流降级。

回答这组问题时,我建议给一个真实的排查记忆:“很多团队上线前没压测过缓存失效场景,结果某天全部热点商品缓存设置同一时间过期,秒杀一开始数据库就被打崩。”这样的例子一出来,整个答案就很有说服力。

6.2 分布式锁面试题:Redis锁从简单到复杂

分布式锁在近几年面试里出镜率极高,而且题目角度很刁钻,经常是“你说说Redis分布式锁怎么做,然后说说它有哪些坑”。这道题想答好,得按版本演进来讲。

第一版:用SETNX加锁,用DEL释放锁。这个版本的问题很致命——如果加锁后的业务代码抛异常,锁永远不会释放,其他线程永远进不来。

第二版:给锁设置过期时间,SET key value EX 30 NX。这样即使业务异常,锁也会自动过期。但问题在于,如果线程A的业务执行时间超过30秒,锁提前过期了,线程B拿到了新锁,此时A执行完释放锁,把B的锁给删了。这就是典型的“误删别人锁”,解决方案是在锁的value里存一个随机值,释放时先比较value一致再删,比较和删除要保证原子性,一般用Lua脚本实现。

第三版:在单机Redis上,上面的方案够用了。但如果面试官继续追问“如果Redis节点宕机怎么办”,就到了RedLock的范畴。RedLock的核心思想是向多个独立的Redis节点请求加锁,超过半数节点加锁成功才算成功。但RedLock本身也存在争议,比如主从切换期间锁可能丢失、GC停顿可能导致锁过期后其他节点又拿到锁、时钟漂移问题等。能说出RedLock的缺陷,这道题基本就过了。

6.3 MySQL索引与事务隔离级别:最容易被追问的DB面试题

MySQL相关的面试题几乎必考索引和事务。索引的基础是B+树,白话解释一下:B+树的每个叶子节点存放真正的数据,非叶子节点只放索引值,这样树的高度很低,三四层就能撑起千万级数据量;叶子节点之间用指针相连,方便范围查询顺序遍历。

在此基础上,三个高频扩展考点:第一个是聚簇索引与非聚簇索引。InnoDB的主键索引就是聚簇索引,叶子节点直接存放整行数据;二级索引叶子节点存放主键值,所以用二级索引查询时,如果查询的列不在索引里,需要根据主键回表查一次。第二个是最左前缀原则。联合索引(a,b,c)在查询时,用a走索引,a和b走索引,但直接用b查就没办法用这个索引,因为B+树先按a排序再按b排序。第三个是覆盖索引。如果查询的所有字段都包含在索引中,就不需要回表,直接用二级索引搞定。

事务隔离级别这部分,MySQL默认是可重复读。四个级别按隔离强度排:读未提交、读已提交、可重复读、串行化。读未提交会导致脏读,读到其他事务未提交的数据;读已提交避免脏读,但可能出现不可重复读,也就是同一个事务内两次读同一行,结果不一样;可重复读避免不可重复读,MySQL通过MVCC多版本控制实现,同一个事务内读到的都是同样的快照;串行化最高,但性能最低。

如果被问到MVCC,简单说法是:每行记录维护多个版本,每次更新会生成新版本,事务的ReadView决定它能看到哪个版本。这也是为什么可重复读级别下,其他事务的修改当前事务看不到,因为ReadView在事务开始时固定了可见版本。

6.4 MyBatis的#{}和${}:以及缓存机制

Java岗位面试里MyBatis出现频率很高,第一道送分题就是#{}和${}的区别。答案就一句话:#{}是预编译占位符,MyBatis会把它替换成问号,再通过PreparedStatement传入参数,能有效防止SQL注入;${}是字符串直接拼接,存在注入风险,只在需要动态指定表名、排序字段这类无法用占位符的场合使用。

缓存机制也是经典考点。MyBatis一级缓存是SqlSession级别的,默认开启,同一个SqlSession内多次执行相同查询,第二次会直接返回缓存结果。坑在于一旦执行了增删改操作,缓存会立刻清空,所以有时候在同一个方法里先查再改再查,第二次查询还是会走数据库。二级缓存是namespace级别的,跨SqlSession共享,默认不开启,开启后有数据一致性问题要注意,在多表操作时特别容易查到脏数据。

答疑技巧:面试时说到MyBatis二级缓存,主动提一句“生产环境里用得少,因为需要自己做数据一致性控制”,面试官就不会继续往深里挖无谓细节。

7. 面试答题技巧与避坑清单

7.1 面试官问“你知道吗”的时候,到底想听什么

聊了这么多具体题目,最后说点面试方法论。很多候选人有个误区,觉得面试就是面试官问一个点,你答一个点,答上就加分,答不上就扣分。实际上大部分面试官在追问时看的是你的思维链路。

比如问“HashMap为什么线程不安全”,如果你只说“并发put会丢数据”,这是合格线。更好的答题方式是:先给结论,然后说JDK7头插法可能形成环、JDK8尾插法不会成环但会丢数据,最后补一句“所以并发场景要用ConcurrentHashMap,它通过CAS和synchronized锁桶来保证安全”。这个结构,结论到原因、历史到现状、问题到解决方案,一整条逻辑链下来,哪怕中间有点小瑕疵,面试官给你的评价也会明显高一个档次。

还有一个实用技巧:当面试官问到你不确定的问题时,先复述一遍你理解的问题,再用“我目前的理解是……,如果哪里不对请指正”作为开头。这样做有两个好处,一是给自己争取几秒钟思考时间,二是先把不确定的范围框住,面试官如果发现方向偏了会纠正你,反而比你含糊其词硬猜好得多。

7.2 从实际面试中总结的高频追问变体

我整理了面试中经常出现的几个“变体”,这些变体往往是候选人挂掉的地方,提前了解能避免踩坑。

第一,关于HashMap加载因子0.75的追问。很多回答说“为了解决时间和空间的权衡”,但面试官会继续问“为什么不是0.5或者1.0”。能稳住就要说清楚:加载因子越高,空间利用率越高,但哈希碰撞概率也越高,查询效率下降;加载因子越低,碰撞少,但浪费空间。0.75是长期实践折中的结果,在JDK源码注释里也有说明。

第二,关于Redis缓存雪崩,如果候选人说了“设置随机过期时间”,面试官很可能追问“随机范围设多大”。遇到这种问题时,不要慌张,给一个基于经验的答案:“一般会在基础过期时间上加一个5%到20%的随机浮动,太小的随机没有分散效果,太大的随机会导致部分缓存过早失效。”

第三,关于Spring事务失效的场景。面试中很常问“为什么自己调自己的另一个方法,事务注解会失效”。因为这个调用绕过了Spring生成的代理对象,相当于this.method()直接调用,没有经过切面,自然没有事务。解决办法是注入自身的代理,或者用AopContext.currentProxy()获取当前代理对象再调方法。

7.3 学习资源和复习节奏建议

准备Java面试没必要把所有资料都刷一遍,我的建议是按“基础+框架+项目”三线并行来安排。基础部分以JVM、并发、集合为主线,用《Java并发编程的艺术》加源码阅读为主;框架部分以Spring和Spring Boot官方文档加源码调试为主,重点理解启动流程和核心注解;项目部分要把自己做的项目按“项目背景、技术选型、核心难点、解决过程”四个维度梳理成文档,面试官问项目时能连贯地讲清楚。

复习节奏上,建议提前一个月开始。前两周按模块过知识点,每个模块列一张思维导图,每天自查“这个点我能给别人讲明白吗”;第三周开始做模拟面试,找人互相提问,或者自己录音复盘,这一步非常重要,能明显发现自己口头表达上的卡壳点;最后一周回归高频题,把热点答案重新口头过一遍,同时保持手写核心源码片段的习惯。

我个人在实际面试中的体会是,准备面试最忌讳的是“我看过但说不清”。平时看博客、刷视频、读源码的时候,多问自己一遍“如果让我现在用白话解释给一个不懂Java的人听,我能不能讲明白”。如果讲不明白,说明这个知识点还没有真正内化。这套2026年Java面试题的整理,我也尽量往这个方向靠,不追求把答案写成论文,而是追求每个考点都能让你在现场用一两分钟说清楚。最后再分享一个小技巧:每道题看完后,合上手机,自己对着空气讲一遍,能顺下来就说明真的记住了。