2026年Java面试高频题汇总:核心原理与实战解析

2026年Java面试题汇总篇——附答案

眼瞅着2026年的招聘季又来了,Java岗位依然是卷中卷,但万变不离其宗。我面试过不少人,也被面过不少次,最大的感受是:面试官越来越不喜欢“背八股”,而是喜欢把一个知识点往深了问、往场景里带。很多候选人基础题都能答上来,一到“为什么”“还有没有更好的方案”就卡壳。

这篇汇总我不打算铺开二三百道题,而是把2026年面试中最常出现的核心点按模块整理出来,每一道都给答案、给原理、给场景,顺便把一些容易忽略、容易踩坑的细节标出来。适合准备春招秋招的同学、想跳槽的初中级工程师,也适合打算系统复盘一遍Java知识体系的老手。文章比较长,建议收藏后按模块分几次看完。

1. Java基础与语言特性高频题

1.1 String为什么是不可变的?真的不可变吗

String类被final修饰,底层用final char[](JDK9以后是final byte[])存储,这是它不可变的两个基石。但面试官真正想问的是“不可变有什么好处”:字符串常量池复用、线程安全、hashCode可以缓存、防止内部数据被篡改。

有一个刁钻的追问我几乎每次面试都会遇到:那我用反射能不能改String的值?答案是能。通过反射拿到value字段,并且setAccessible(true),就能修改内部数组的元素。所以String的不可变是“设计层面的不可变”,不是“物理层面的绝对不可变”。

1.2 equals和hashCode为什么必须一起重写

因为HashMap、HashSet这类散列集合依赖这两个方法的约定:两个对象equals相等,hashCode必须相等;反过来不成立。如果只重写equals不重写hashCode,两个业务上相等的对象会落到HashMap的不同桶里,导致get不到值、Set里出现重复元素。

实操中我用Lombok的@EqualsAndHashCode比较多,但有个坑:如果类继承了一个父类,且父类也有字段参与equals比较,Lombok默认会调用父类的equals方法,容易造成较隐蔽的问题。遇到继承关系,我一般手动重写并把父类的字段比较也带进来。

1.3 Java异常体系:受检异常到底该不该用

Exception分两类:受检异常(Checked)和运行时异常(RuntimeException)。受检异常强制调用方处理,比如IOException、SQLException;非受检异常不强制处理,比如NullPointerException、IllegalArgumentException。

2026年的主流实践已经偏向“拥抱非受检异常”,Spring的@Transactional默认只对RuntimeException回滚,这也从侧面说明业界倾向。我的经验是:能恢复的场景用受检异常,调用方有能力处理;不能恢复的(参数错误、空指针、状态非法)直接用非受检异常,别让上层做无意义的try-catch。

1.4 Lambda和函数式接口:不只是语法糖

很多候选人知道Lambda怎么用,但解释不了它和匿名内部类的本质区别。Lambda不是简单的语法糖,invokedynamic指令在运行时才会生成实现类,这点和匿名内部类编译期就生成class文件完全不同。所以Lambda的实例复用性能更好、堆内存占用更少。

函数式接口是只含一个抽象方法的接口,比如Runnable、Consumer、Supplier、Function、Predicate。Java 8以后我们可以用@FunctionalInterface注解标记自定义函数式接口。实际开发中我经常用Function来做“参数转换器”,用Predicate做“条件过滤器”,配合Stream使用能让代码清爽很多。

2. 集合框架:HashMap是永远绕不开的坎

2.1 HashMap底层结构:JDK 1.7和1.8的关键差异

这是Java面试八股文里的“题眼”,几乎每场必问。JDK 1.7的HashMap底层是数组加链表,头插法;JDK 1.8以后改成数组加链表加红黑树,尾插法。链表长度超过8且数组长度超过64时,链表转红黑树;红黑树节点数低于6时退化为链表。

我建议你把这个8和64的来源搞清楚:8是泊松分布算出来的概率阈值,链表长度达到8的概率只有千万分之六,选择8是为了在时间和空间上取得平衡。数组长度小于64时先扩容而不是转树,是因为链表短时树化反而增加开销。面试时能把这一层说清楚,基本就赢过大多数人。

2.2 HashMap扩容机制:为什么容量必须是2的幂

HashMap默认容量16,负载因子0.75,也就是元素个数超过16乘0.75等于12时触发扩容,每次容量翻倍。元素定位用的是(n-1) & hash,而不是取模,因为位运算更快,而且只有容量是2的幂时(n-1)才能保证低位全是1,散列才均匀。

扩容不是简单复制,JDK 1.8会对链表进行拆分,一部分留在原位置,一部分移动到“原位置加旧容量”的新位置。原因也很简单:扩容后n-1的高位多了一个1,旧hash对应位是0还是1决定了去留。每次扩容都要重新计算hash? 不用,JDK 1.8用e.hash & oldCap判断,结果0留原位,非0去新位。

2.3 ConcurrentHashMap:锁粒度是怎么一步步优化的

JDK 1.7的ConcurrentHashMap用Segment分段锁,默认16段,把锁的粒度从整张表降到了段级别。JDK 1.8直接用CAS加synchronized锁单个桶,锁粒度更细了。put时如果桶为空,用CAS直接插入;桶不为空,锁住链表或红黑树的头节点,再执行插入。

把这段说清楚就足够有区分度了。synchronized在JDK 1.6之后经过锁升级,重量级锁已经没那么“重”,所以JDK 1.8的ConcurrentHashMap退回了synchronized加CAS的方案,性能和代码复杂度得到了更好的平衡。

2.4 ArrayList扩容和元素删除的坑

ArrayList默认容量10,添加第11个元素时触发扩容,新容量是旧容量的1.5倍,也就是oldCapacity加oldCapacity右移一位。扩容的本质是Arrays.copyOf生成新数组,这是O(n)操作,频繁扩容会拉低性能。如果你能预判元素规模,建议直接new ArrayList<>(expectedSize)。

还有一个高频坑:用for循环删除ArrayList元素时,删掉一个元素后面的会自动前移,索引要i--,否则会漏删。正确做法是使用Iterator的remove方法,或者在JDK 8里用removeIf一行搞定。我建议基本功不扎实的人别在简历里写“精通集合源码”,这个模块太容易被往深了问。

3. JVM与性能优化:从内存区域到OOM实战

3.1 运行时数据区:哪些线程私有,哪些线程共享

JVM内存区域是必考题。线程私有的有虚拟机栈、本地方法栈、程序计数器;线程共享的堆、方法区。JDK 8以后方法区被元空间取代,元空间使用本地内存,不再受堆大小限制,默认情况下只受物理内存约束。

很多人分不清堆和栈,我用一个生活类比:栈是“方法调用的舞台”,一个方法调用压一个栈帧,方法结束帧就弹出;堆是“对象的仓库”,所有new出来的对象都放这里,由垃圾收集器统一管理。程序计数器记录当前线程执行到哪一条字节码指令,异常处理、线程切换都靠它,是唯一不会OOM的区域。

3.2 什么时候触发Young GC和Full GC,垃圾收集器怎么选

新对象分配在Eden区,Eden满时触发Minor GC,存活对象进入Survivor区,年龄加1,达到15岁(可配-XX:MaxTenuringThreshold)进入老年代。大对象直接进老年代,这个阈值可以通过-XX:PretenureSizeThreshold设置,我一般在处理大缓存对象时会特意调一下。

Full GC的触发条件包括老年代空间不足、元空间不足、System.gc()等。2026年面试里比较爱问G1和ZGC。G1把堆分成大小相等的Region,可以设置-XX:MaxGCPauseMillis来指定停顿目标。ZGC用了着色指针和读屏障,号称停顿时间不超过10毫秒,但它更适合大堆、低延迟的场景。

3.3 线上OOM怎么排查:从命令行到监控平台

我见过很多面试者能背出OOM类型,但遇到真实问题就手忙脚乱。常见的OOM有四类:Java heap space堆内存不足、Metaspace元空间不足、GC overhead limit exceeded(GC回收效果差)、unable to create new native thread无法创建线程。

排查顺序我一般这么走:先用free -h看机器内存,再用jmap -heap查看堆使用情况,接着jps找到进程ID,jstack导线程栈,最后jmap -dump:format=b,file=heap.hprof导出堆快照,用MAT或者JProfiler分析。新手最容易漏掉的一点是:先看是不是代码问题,而不是直接加内存。我之前排查过一个“OOM”问题,最后发现是for循环里不断往List塞数据,内存被活对象占满了,这种问题加多少内存都没用。

3.4 类加载机制和双亲委派模型

类加载分加载、验证、准备、解析、初始化五个阶段,双亲委派说的是:一个类加载器收到加载请求时,先委托给父类加载器,父类加载不了才自己加载。这样做的核心目的是保证Java核心类库的安全,比如java.lang.String永远由启动类加载器加载,防止有人写个同名类替代。

面试喜欢追问“能不能打破双亲委派”,答案是可以,不过要自己实现ClassLoader并重写loadClass方法。典型场景是Tomcat的WebAppClassLoader、JDBC的SPI机制、以及热部署框架。聊到这里我会顺带提一句:真正需要打破双亲委派的场景其实很少,面试官如果想深挖,重点是在“为什么需要SPI反着加载驱动类”。

4. Spring与Spring Boot核心考点

4.1 IOC和AOP:理解背后的核心思想

IOC(控制反转)是把对象的创建和管理权交给容器,开发人员只声明依赖,容器负责注入。AOP(面向切面编程)把日志、事务、权限等横切逻辑从业务代码中抽离出来,用动态代理在运行时织入。

源码层面有两个关键类:BeanFactory是顶层接口,提供最简单的容器能力;ApplicationContext在它之上加了事件发布、国际化、环境配置等能力。AOP的底层是JDK动态代理和CGLIB:目标类有接口用JDK代理,没有接口用CGLIB,Spring Boot 2.x以后默认用CGLIB。

4.2 Bean的生命周期:完整链路与常见误区

Spring Bean的生命周期是高级面试里最常考的点之一。完整链路很长,但核心节点就几个:实例化、属性填充、初始化(包括BeanPostProcessor的前后置处理、afterPropertiesSet、init-method)、使用、销毁。

我建议你重点记BeanPostProcessor这个扩展点,Spring的AOP、@Autowired、@Resource全是通过各种BeanPostProcessor实现的。有一个常见误区必须纠正:构造方法是在实例化阶段调用的,属性填充发生在构造之后,所以千万不要在构造方法里使用依赖属性,那一定是null。

4.3 Spring如何解决循环依赖:三级缓存是重点

Spring解决不了构造器循环依赖,只能解决setter/字段注入的循环依赖。核心是三个Map,也就是三级缓存:singletonObjects(完整单例)、earlySingletonObjects(提前暴露的半成品)、singletonFactories(对象工厂)。

流程大概是:A创建时发现依赖B,先从三级缓存拿到B的早期引用并放入二级缓存;B创建时依赖A,从二级缓存拿到A的早期引用完成创建;B完整创建后,A再从缓存里拿B的完整对象。难点在于:为什么需要三级而不是二级?答案是为了支持AOP代理,如果不需要代理,二级缓存就够了,但代理对象必须在早期就暴露。

4.4 @Transactional事务失效的N种姿势

Spring事务失效太常见了,我面试时必问这个问题。失效场景大概有这几种:方法不是public、方法被final修饰、类没有被Spring管理、自己new的对象调用、同类内部方法调用(this调用绕过代理)、异常被catch吞掉、抛出的是受检异常且没有配置rollbackFor、数据库引擎不支持事务(比如MyISAM)。

其中同类内部方法调用是最高频的坑。我自己就踩过:A方法里调用this.b(),b上有@Transactional,结果事务完全不生效。原因很简单,Spring事务是基于代理的,外部调用才走代理,内部this调用走的是原生对象。解决办法是注入自身或者拆到另一个Service里。

4.5 Spring Boot自动配置原理

Spring Boot的自动配置核心是@EnableAutoConfiguration注解,它通过SpringFactoriesLoader机制加载META-INF/spring.factories文件里的自动配置类。每个自动配置类上都有@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean等条件注解,只有满足条件时才生效。

我的理解可以总结成一句话:Spring Boot把“常用框架的默认配置”做成了可自动装配的模块,你不用写XML配置,只要引入依赖,框架自动帮你配好,需要自定义时用配置项或@Configuration覆盖默认值。

5. MySQL:索引、事务与SQL优化

5.1 B+树索引为什么快,和B树的区别在哪

MySQL InnoDB引擎的索引用B+树,而不是B树、红黑树或哈希索引。核心区别是:B+树的所有数据都存在叶子节点,非叶子节点只存索引键;叶子节点之间有双向链表连接,方便范围查询。B树的非叶子节点也存数据,树的层数更多,磁盘IO次数更多。

哈希索引适合等值查询,不支持范围查询和排序,所以InnoDB的默认索引结构不用哈希。你可以这么理解:B+树牺牲了一点随机查找性能,换来了极其高效的范围扫描和更低的树高度,而SQL查询里有大量范围操作,B+树完美匹配这种场景。

5.2 索引失效的典型场景

我在面试时特别爱问“索引失效的场景”,因为这是真实的开发高频问题,而且能检验一个人到底有没有真正调过SQL。常见失效场景包括:对索引列使用函数或表达式计算、隐式类型转换、左模糊查询%xxx、or连接非索引列、复合索引不满足最左前缀原则、条件里出现is not null(视版本和优化器而定)。

还有两个容易被忽略的坑:一是索引列参与运算,比如where age + 1 = 20,索引会失效,应该改成where age = 19;二是范围查询以后的条件不再走索引。写SQL时先用explain看执行计划,type至少要达到range或者ref,看到all就要警惕全表扫描了。

5.3 事务隔离级别与MVCC

MySQL默认隔离级别是可重复读(Repeatable Read),事务隔离级别从低到高包括读未提交、读已提交、可重复读、串行化。隔离级别解决三个问题:脏读、不可重复读、幻读。

MVCC(多版本并发控制)是InnoDB实现高并发读的关键,核心机制包括隐藏字段(DB_TRX_ID、DB_ROLL_PTR)、undo log和read view。可重复读通过事务第一次select时生成read view,后续select都复用这个视图,从而保证快照读的一致性。MVCC解决的只有快照读,对当前读的幻读还是要靠间隙锁和next-key lock。

5.4 SQL优化实战:一条慢查询的完整排查流程

从简历到实际场景,SQL优化一定跑不掉。我一般按这个顺序排查:先开慢查询日志,找到具体SQL;然后explain看执行计划;再判断是索引问题还是SQL写法问题;如果是数据量太大,接着考虑分页优化、缓存、读写分离、分库分表。

举个例子:一条订单列表查询,where条件里有用户ID和状态,order by时间,limit 100000, 20,这个深分页会导致扫描大量数据。我的优化方案是子查询延迟关联,先查出主键ID再做join,或者用游标方式记住上一页的最后更新时间。实际效果从3秒能压到0.1秒以内,提升非常明显。面试时能讲出这类实战优化案例,比单纯背八股要加分得多。

6. Redis:缓存穿透、击穿、雪崩与分布式锁

6.1 三大缓存问题:概念、区分与解决方案

缓存穿透:查询一个不存在的key,请求直接打到数据库。方案有布隆过滤器、缓存空值(设置短过期时间)、参数校验。

缓存击穿:一个热点key过期,大量请求同时打到数据库。方案有互斥锁、逻辑过期不用物理过期(后台异步续期)。

缓存雪崩:大量key同时过期,或者Redis实例宕机,请求全部打到数据库。方案有过期时间加随机值、多级缓存、Redis集群高可用、限流降级。

面试时把三个问题区分清楚是第一步,更重要的是说出“我怎么判断是哪种问题,以及为什么选这个方案”。我习惯先问一句:你们的服务是读多写少还是写多读少?不同的业务模型,缓存方案完全不同。

6.2 Redis持久化:RDB和AOF怎么选

RDB是定期生成快照,优点是文件小、恢复快,缺点是可能丢失最后一次快照之后的数据。AOF记录每次写命令,数据安全性更高,可以选择每次写都刷盘,但文件体积大、重放慢。

2026年的主流实践是AOF加RDB的混合持久化,Redis 4.0以后支持。AOF重写时会生成一个RDB格式的基础文件,再追加增量命令。这样重启恢复时既有RDB的恢复速度,又有AOF的数据安全性。实际线上我一般设置appendfsync everysec,兼顾性能和安全性。

6.3 Redis分布式锁:从SETNX到Redisson

早期分布式锁用SETNX加expire两步,但两步不是原子的。后来用SET lockKey value NX EX 30解决原子性。但还有个问题:如果业务执行超过30秒,锁自动释放,其他线程拿到锁,就会出并发安全问题。解决方案是Redisson的看门狗机制,默认每10秒续期一次,保证业务没结束锁不释放。

还有一个隐藏考点:锁的value怎么设计?我建议用UUID加线程ID,保证只有持锁人才能释放锁。释放时先比对value再del,这两步也要保证原子性,最简单的方式是用Lua脚本。Redis集群场景下,Redisson还提供了红锁RedLock方案,但说实话,我对红锁持保留意见,它复杂、低效,不是所有场景都需要。

6.4 Redis为什么快:单线程模型的真相

Redis的单线程说的是处理网络请求和命令执行是单线程,但持久化、异步删除、AOF重写是另外的线程。所以“Redis是单线程的”这句话严谨来说是对的,但容易误导人。

Redis快的核心原因:基于内存、IO多路复用、单线程避免上下文切换和锁竞争、高效的数据结构。IO多路复用模型是面试重点,可以简单理解为“一个线程同时监听多个socket事件,谁有数据就处理谁”,epoll是Linux下最高效的实现。

7. Kafka消息队列面试题

7.1 Kafka为什么用分区和副本,会丢消息吗

Kafka的Topic分成多个分区,分区是并行和扩展的最小单位。每个分区有多个副本,副本之间是一主多从关系,只有Leader副本负责读写,Follower只做同步。分区让我们可以水平扩展,副本让我们能容忍节点故障。

消息不丢失是个系统工程,要从生产者、Broker、消费者三个层面保证。生产者用acks=all加retries;Broker设置min.insync.replicas=2,并启用 unclean.leader.election.enable=false,避免选举出落后很多的副本;消费者手动提交offset,业务处理成功后再提交。

7.2 消费组的rebalance和offset管理

消费组里的每个分区同一时间只能被组内一个消费者消费。当消费者数量变化、分区数量变化、订阅关系变化时,触发rebalance,把分区重新分配。rebalance期间消费是停止的,所以频繁rebalance是性能杀手。

offset是每个消费者在分区上的消费位置,存在Kafka内部主题__consumer_offsets里。手动提交时最容易踩坑的是“提交时机”:先处理业务再提交offset,会有重复消费的风险;先提交再处理业务,会丢消息。通常情况下我选择处理完业务再提交,配合幂等消费解决重复问题。

7.3 顺序消费和重复消费怎么处理

Kafka本身不保证全局顺序,但能保证单分区内的顺序。需要顺序的场景,我一般用“按业务ID取模选分区”的方式,让同一订单的消息进同一个分区。

重复消费是消息队列不可回避的问题。我处理过最典型的场景是支付回调,重复消息导致重复发放积分。我的方案是:消费端不只是做判断,而是把消息的唯一键(订单号、流水号)作为幂等键写入Redis Set,用setnx保证同一消息只处理一次;更严格场景配合数据库的唯一索引,双保险。

8. 微服务与分布式:从Spring Cloud到分布式事务

8.1 服务注册与发现为什么要用注册中心

微服务拆开后,服务实例的IP和端口是动态变化的,不能写在配置里写死。注册中心(Nacos、Eureka、Consul)解决的就是服务发现、健康检查和配置管理问题。服务启动时向注册中心注册自己,服务消费者从注册中心拉取服务列表,再配合负载均衡选一个实例调用。

Nacos和Eureka的主要区别是:Nacos支持AP和CP两种模式切换,Eureka只有AP模式;Nacos还内置了配置中心功能。2026年我用的比较多的是Nacos加Spring Cloud Alibaba这套组合。

8.2 接口幂等性和分布式事务

幂等性指的是“同样的请求执行一次和执行多次,结果一样”。幂等方案有:数据库唯一索引、Redis setnx、状态机(订单状态从待支付到已支付,已支付状态不能再改)、token机制(先获取token再带token请求)。

分布式事务的经典方案有三种:2PC/XA强一致(适合对一致性要求极高的场景,性能差)、TCC补偿(Try-Confirm-Cancel,实现复杂,适合金融系统)、最终一致性(本地消息表、事务消息、定时任务补偿)。Alibaba的Seata是主流实现,支持AT、TCC、SAGA、XA四种模式,我一般业务上优先用AT模式,配合全局锁和undo_log实现,入侵性小。

8.3 限流、熔断、降级的区别

限流是控制流量速率,比如每秒只允许1000个请求;熔断是依赖服务故障时快速失败,不再继续调用;降级是在高峰期牺牲非核心功能。三者的关系可以这么理解:限流防“流量过载”,熔断防“故障扩散”,降级保“核心可用”。

常用组件有Sentinel和Hystrix。Sentinel是阿里开源,支持流控、熔断降级、系统自适应保护,配置粒度更细,还能和Spring Cloud Gateway、Dubbo整合。Hystrix已经停止维护,除非遗留项目,新项目我不建议使用。

9. Linux与网络基础:面试必带的一波送分题

9.1 高频Linux命令查漏补缺

Java开发最常见的Linux操作是看日志、查进程、查端口、看资源使用。我把最常用的命令列一下:

# 查看端口占用 netstat -tunlp | grep 8080 lsof -i:8080 # 查看进程 ps -ef | grep java top -p 12345 # 查看日志 tail -f app.log grep "ERROR" app.log | tail -100 # 磁盘和内存 df -h free -h

还有一个特别实用的:生产环境Java进程突然CPU飙升,先top找到PID,再top -Hp pid看到具体线程,然后jstack导出线程栈,用线程ID十六进制grep定位代码行。这整套流程面试必问,结合实战讲出来很有说服力。

9.2 TCP三次握手、四次挥手与TIME_WAIT

TCP三次握手是建立连接的过程:客户端发SYN,服务端回SYN+ACK,客户端再回ACK。为什么不能两次?因为两次握手无法确认客户端的接收能力。四次挥手是断开连接的过程,多出来的原因在于TCP是全双工,需要两个方向都关闭。

TIME_WAIT这个状态值得单独拎出来聊。主动关闭连接的一方进入TIME_WAIT,持续2MSL(默认60秒)。为什么要有这个状态?一是保证最后一个ACK能被服务端收到,如果丢了能重发;二是让旧连接的报文在网络中彻底消失,防止影响新连接。高并发短连接场景下,大量TIME_WAIT会占满端口,解决办法有开启tcp_tw_reuse、长连接、或调小MSL,但都要谨慎。

9.3 HTTP和HTTPS的区别

HTTP是明文传输,HTTPS在HTTP和TCP之间加了一层TLS/SSL协议,通过混合加密保证传输安全:用非对称加密协商出对称密钥,后续用对称加密传输数据。CA证书的作用是验证服务端身份,防止中间人攻击。

面试时容易忽略的点:HTTPS比HTTP多了2次RTT的握手开销,首次访问会明显变慢。优化方案是开启TLS 1.3、启用会话复用、用HTTP/2多路复用。前端面试题里也有不少涉及这边的内容,比如浏览器缓存、HTTP状态码,这部分我建议前后端都看一眼。

10. 场景设计题与手写算法题

10.1 设计一个秒杀系统:从前端到数据库的层层拦截

秒杀系统面试题非常高频,考察的不是某个框架,而是全链路思维。我的设计思路是“层层拦截,把流量挡在最前面”:前端做按钮置灰和验证码,网关层做限流,Redis预扣库存,MQ异步下单,数据库做最终扣减,对账任务兜底。

库存扣减是关键,不能直接操作数据库,否则并发一上来就崩。正确做法是:系统启动时把库存加载到Redis,用Lua脚本原子扣减库存,扣减成功才发MQ消息,消费端再异步落库。超卖问题靠Redis的原子操作和数据库的乐观锁解决。这样的系统设计,核心是“牺牲部分一致性,换取整体可用性”。

10.2 手写单例模式:双重检查锁为什么要加volatile

单例模式是手写题里的常客。双重检查锁写法:

public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

volatile在这里的作用是禁止指令重排序。new Singleton()不是原子操作,实际分三步:分配内存、初始化对象、将引用指向内存。如果重排序成“先指向内存再初始化”,另一个线程可能拿到一个未初始化完成的对象。这个考点既考并发,又考JMM,我面试时只要候选人能说清volatile的作用,这题基本就算过了。

10.3 排序算法:冒泡排序和快速排序的边界

手写排序题里,快速排序出现的频率最高,但冒泡排序也常被用来做热身。我先给快排的实现:

public static void quickSort(int[] arr, int left, int right) { if (left >= right) return; int i = left, j = right, pivot = arr[left]; while (i < j) { while (i < j && arr[j] >= pivot) j--; while (i < j && arr[i] <= pivot) i++; if (i < j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } arr[left] = arr[i]; arr[i] = pivot; quickSort(arr, left, i - 1); quickSort(arr, i + 1, right); }

快排平均时间复杂度O(nlogn),最坏O(n²),最坏情况对应的是每次选的pivot都是最大或最小元素(比如数组已经有序时固定取第一个元素)。优化思路:随机取pivot、三数取中、小数组切换到插入排序。冒泡排序虽然简单,但要注意边界条件处理,还要能说出时间复杂度O(n²)、稳定排序这些基础结论。

10.4 TopK问题:海量数据找最大的K个数

TopK问题考验的是算法思维和复杂度分析。如果数据能全放内存,用大小为K的小顶堆,遍历一遍数据,堆顶是最小值,遇到比堆顶大的替换并调整,时间复杂度O(nlogK)。如果数据量大到内存放不下,就用分治法拆成小文件,每个文件算TopK,再合并。

还有快排的partition法,平均O(n),但会修改原始数组。实际业务里我用Redis的ZSET做过TopK,用O(logN)完成插入和排序,但在面试里重点还是要讲原理。优先级队列PriorityQueue本身就是小顶堆,代码实现只要十行左右。

11. 2026年Java面试的新趋势与准备建议

11.1 八股文之外,面试官在考什么

这两年明显感觉到,纯八股面试在减少,项目深挖和场景题在增加。同样是问Redis,以前问“Redis有哪些数据结构”,现在问“你们项目的缓存怎么设计的、怎么保证一致性、缓存满了怎么处理”。同样问MySQL,以前问“索引有哪些类型”,现在问“你线上慢SQL是怎么优化的、explain里哪些字段重点看”。

我给准备面试的人一个方向:每一个知识点,至少准备三层——是什么、为什么、什么场景下用。比如ArrayList,不能只答“动态数组”,还要能说扩容机制、与LinkedList的对比、什么业务场景选哪个。每一个简历里写的项目,都要能回答三件事:项目里最有挑战的点、你怎么解决的、有没有其他方案。

11.2 一些容易被忽略但实际高频出现的面试题

我整理了最近面试中特别容易被问、但很多人准备不到的问题:

  • Java 8和Java 17、Java 21的版本差异有哪些?比如Java 17中的密封类、Java 21中的虚拟线程。
  • 反射的缺点是什么?为什么框架喜欢用反射?
  • Spring的@Autowired和@Resource有什么区别?按类型还是按名称?
  • MyBatis的#{}和${}有什么区别?为什么${}有SQL注入风险?
  • 数据库连接池为什么用HikariCP?它比你自写的连接池好在哪?
  • 接口的默认方法是什么?为什么要有默认方法?
  • Stream的并行流parallelStream怎么用?什么场景下不能用?

比如虚拟线程,这是Java 21的正式特性,也是2026年面试的新宠。它由JVM调度,而不是操作系统线程,创建成本极低,适合IO密集型任务。你可以把虚拟线程理解为“轻量级的线程”,一个平台线程上可以跑成千上万个虚拟线程,极大提升了并发能力。出现这个考点说明面试已经开始跟着JDK版本走了,抱着一本Java 8的面试书是不够的。

11.3 简历上别写“精通”,但别让自己停留在“会用”

我一直建议候选人把简历上的技能描述写具体,比如“熟悉Java并发编程,理解AQS、synchronized锁升级、ThreadLocal原理,并在xx项目中使用CompletableFuture优化过接口性能”。这种写法比“熟悉Java”有价值得多,而且面试官顺着这个方向问,你也有话可讲。

另一方面,不要光背题。我见过太多候选人,原理倒背如流,但代码能力不行。手写一个多线程交替打印、写一个简单的LRU缓存、用一个Stream做分组统计,都能筛掉不少人。所以我建议准备面试时,每个知识点都配合一个能跑通的小demo,光背不写,面试很容易露馅。

12. 面试答题技巧与最后的避坑提醒

12.1 遇到不会的题,怎么回答才不扣分

面试时最忌讳的是不懂装懂、胡编乱造。我的经验是:先坦诚说这块我没深入用过,然后尝试从已知的知识去推理,再表达自己愿意去学习的意愿。比如面试官问OceanBase相关的题,如果你没接触过分布式数据库,可以这么答:“OceanBase我了解得不多,但我知道它是一款分布式关系型数据库,基于Paxos协议实现高可用。如果碰到类似问题,我会先从它和MySQL的兼容性、数据分布策略、以及读写链路这几个方向去了解。”

这个回答虽然没有给出具体答案,但展现了思考框架和学习的主动性,面试官通常不会因为“不会”直接否定你,反而会因为“逻辑清晰”加分。

12.2 面试中的项目经验怎么讲才出彩

项目讲解建议用STAR法则:背景(Situation)、任务(Task)、行动(Action)、结果(Result)。重点在行动和结果上,结果尽量量化,比如“接口QPS从500提升到2000”“查询耗时从3秒降到200毫秒”“系统可用性从99.9%提升到99.99%”。

讲项目的过程中,主动引导面试官到你有深度的方向。比如你做过Redis缓存,就多聊缓存一致性和击穿处理,主动阐述你的取舍思路。很多候选人把项目讲成了“流水账”,什么模块都沾一遍,面试官听完不知道你具体做了什么,这是最吃亏的。

12.3 准备面试题库的几个实用建议

我对刷题库的看法很明确:题库是地图,不是终点。用题库发现自己不会的点,然后去查源码、看官方文档、写demo验证,这才能转化为真正的能力。纯粹背题,面试中稍微换个问法就会露馅。

最后再分享一个我自己的备考习惯:每准备一个知识点,就输出成一段不超过5分钟能讲完的“口头小讲解”,讲给身边人听或者对着录音笔讲。这个过程会暴露你很多“以为自己懂、其实讲不清”的地方。把那些卡壳的点一个个补上,面试状态会越练越稳。这套方法我从第一次跳槽用到现在,亲测有效。