前言

前言

这是 JavaGuide 面试突击版本,只保留最常问的面试题,并对重点内容进行了 ⭐️ 标注。提供亮色和暗色两个主题,需要打印的朋友请选择亮色版本。

时间充裕的朋友,推荐使用 JavaGuide 网站系统学习,内容更全面深入。

JavaGuide

如果你想要付费支持/面试辅导(比如简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

知识星球

面试突击最新版可在公众号回复「PDF」获取(知识星球会提前同步最新版)。

JavaGuide 公众号

重要说明

本站所有面试题保持年度系统性优化完善,严格同步 Java 技术生态与招聘市场的最新动态,确保内容时效性与前瞻性。

《SpringAI 智能面试平台+RAG 知识库》

这部分内容摘自 JavaGuide 的 Java 新特性专题,并按照面试中出现的频率重新筛选和组织:

JavaGuide

Java 新特性专题:Java 8 到 Java 26 重要特性梳理

Java 新特性专题:Java 8 到 Java 26 重要特性梳理

Java 8 新特性实战

Java 8 新特性实战

Java 17 新特性概览

Java 17 新特性概览

Java 21 新特性概览

Java 21 新特性概览

Java 25 新特性概览

Java 25 新特性概览

Java 26 新特性概览

Java 26 新特性概览

Java 新特性不适合按版本逐条背诵。面试准备时,建议先掌握 Java 8 的函数式编程,再理解 Java 9 到 17 的语言演进,最后重点关注 Java 21 的虚拟线程和模式匹配。Java 25、Java 26 的实际面试频率暂时不高,能说清主要正式特性以及正式、预览和孵化状态的区别即可。

版本选择与学习重点

版本选择与学习重点

⭐️Java 8 之后有哪些重要的新特性?

⭐️Java 8 之后有哪些重要的新特性?

下面这张表只保留值得优先准备的内容,不代表对应版本的全部更新。

Java 版本需要重点掌握的特性Java 8Lambda、函数式接口、Stream、Optional、新日期时间 API、接口默认方法、CompletableFutureJava 9模块系统、集合工厂方法、JShell、接口私有方法Java 10局部变量类型推断 varJava 11标准 HTTP Client、字符串 API 增强、单文件源码运行Java 14Switch 表达式正式转正Java 15文本块正式转正Java 16Record、instanceof 模式匹配正式转正Java 17密封类正式转正、JDK 内部 API 强封装Java 21虚拟线程、Record 模式、Switch 模式匹配、Sequenced Collections、分代 ZGCJava 24虚拟线程在 synchronized 场景下不再固定平台线程Java 25Scoped Values、紧凑源文件、模块导入声明、灵活的构造函数体、紧凑对象头、分代 ShenandoahJava 26HTTP Client 支持 HTTP/3、G1 吞吐优化、AOT 对象缓存支持任意 GC、移除 Applet API

Java 版本需要重点掌握的特性

Java 版本

需要重点掌握的特性

Java 8Lambda、函数式接口、Stream、Optional、新日期时间 API、接口默认方法、CompletableFuture

Java 8

Lambda、函数式接口、Stream、Optional、新日期时间 API、接口默认方法、CompletableFuture

Java 9模块系统、集合工厂方法、JShell、接口私有方法

Java 9

模块系统、集合工厂方法、JShell、接口私有方法

Java 10局部变量类型推断 var

Java 10

局部变量类型推断 var

var

Java 11标准 HTTP Client、字符串 API 增强、单文件源码运行

Java 11

标准 HTTP Client、字符串 API 增强、单文件源码运行

Java 14Switch 表达式正式转正

Java 14

Switch 表达式正式转正

Java 15文本块正式转正

Java 15

文本块正式转正

Java 16Record、instanceof 模式匹配正式转正

Java 16

Record、instanceof 模式匹配正式转正

instanceof

Java 17密封类正式转正、JDK 内部 API 强封装

Java 17

密封类正式转正、JDK 内部 API 强封装

Java 21虚拟线程、Record 模式、Switch 模式匹配、Sequenced Collections、分代 ZGC

Java 21

虚拟线程、Record 模式、Switch 模式匹配、Sequenced Collections、分代 ZGC

Java 24虚拟线程在 synchronized 场景下不再固定平台线程

Java 24

虚拟线程在 synchronized 场景下不再固定平台线程

synchronized

Java 25Scoped Values、紧凑源文件、模块导入声明、灵活的构造函数体、紧凑对象头、分代 Shenandoah

Java 25

Scoped Values、紧凑源文件、模块导入声明、灵活的构造函数体、紧凑对象头、分代 Shenandoah

Java 26HTTP Client 支持 HTTP/3、G1 吞吐优化、AOT 对象缓存支持任意 GC、移除 Applet API

Java 26

HTTP Client 支持 HTTP/3、G1 吞吐优化、AOT 对象缓存支持任意 GC、移除 Applet API

JDK 8 到 JDK 25 的发布节奏

如果面试时间有限,优先级可以简单记为:Java 8 > Java 21 > Java 17 > Java 11 > Java 25,Java 26 了解即可。Java 26 不是 LTS 版本,相关内容目前还很少成为常规面试题,不需要逐项背诵 JEP。

LTS 版本是什么意思?Java 的 LTS 版本有哪些?

LTS 版本是什么意思?Java 的 LTS 版本有哪些?

LTS 是 Long-Term Support 的缩写,表示发行方会对某个版本提供更长时间的更新和支持。这里要注意:LTS 不是 OpenJDK 项目给所有发行版作出的统一承诺,具体支持周期、补丁策略和许可证取决于 JDK 发行方。

以 Oracle 的支持路线为例,Java 8、11、17、21 和 25 是 LTS 版本。自 Java 9 起,JDK 大致每 6 个月发布一个功能版本;非 LTS 版本仍然是正式版本,只是支持周期通常更短。

项目选型不能只看“是不是 LTS”,还要看框架、依赖、构建工具、部署环境和 JDK 发行版的支持情况。已有项目通常选择生态成熟的 LTS 版本,新项目则优先考虑当前主流 LTS 版本。

Java 8 核心新特性

Java 8 核心新特性

⭐️什么是 Lambda 表达式?

⭐️什么是 Lambda 表达式?

Lambda 表达式可以把一段行为作为参数传递,是函数式接口实例的一种简洁表达。它常用于替代只实现一个抽象方法的匿名内部类。

List<String> names = Arrays.asList("Java", "Go", "Rust");
names.forEach(name -> System.out.println(name));

// 方法引用是 Lambda 的一种简化写法
names.forEach(System.out::println);
List<String> names = Arrays.asList("Java", "Go", "Rust");
names.forEach(name -> System.out.println(name));

// 方法引用是 Lambda 的一种简化写法
names.forEach(System.out::println);

Lambda 不是普通意义上的“独立函数”,它需要目标类型。下面的表达式能够成立,是因为编译器从变量类型中知道它要创建的是 Predicate<String>:

Predicate<String>
Predicate<String> notEmpty = value -> !value.isEmpty();
Predicate<String> notEmpty = value -> !value.isEmpty();

什么是函数式接口?

什么是函数式接口?

函数式接口是只有一个抽象方法的接口,也叫 SAM(Single Abstract Method)接口。它可以包含默认方法、静态方法,以及从 Object 继承的方法。

Object

@FunctionalInterface 不是函数式接口成立的必要条件,但建议加上。这样一旦接口中意外出现多个抽象方法,编译器就会直接报错。

@FunctionalInterface

Java 8 在 java.util.function 包中提供了很多通用函数式接口,最常用的有:

java.util.function

接口核心方法用途Function<T, R>R apply(T t)接收一个值并转换为另一个值Consumer<T>void accept(T t)消费一个值,不返回结果Supplier<T>T get()不接收参数,提供一个值Predicate<T>boolean test(T t)对一个值进行条件判断

接口核心方法用途

接口

核心方法

用途

Function<T, R>R apply(T t)接收一个值并转换为另一个值

Function<T, R>

Function<T, R>

R apply(T t)

R apply(T t)

接收一个值并转换为另一个值

Consumer<T>void accept(T t)消费一个值,不返回结果

Consumer<T>

Consumer<T>

void accept(T t)

void accept(T t)

消费一个值,不返回结果

Supplier<T>T get()不接收参数,提供一个值

Supplier<T>

Supplier<T>

T get()

T get()

不接收参数,提供一个值

Predicate<T>boolean test(T t)对一个值进行条件判断

Predicate<T>

Predicate<T>

boolean test(T t)

boolean test(T t)

对一个值进行条件判断

为什么 Lambda 只能捕获 final 或 effectively final 的局部变量?

为什么 Lambda 只能捕获 final 或 effectively final 的局部变量?

Lambda 可以访问外部局部变量,但该变量必须是 final 或 effectively final,也就是初始化后没有再次赋值。

final
int minLength = 3;
Predicate<String> valid = value -> value.length() >= minLength;
int minLength = 3;
Predicate<String> valid = value -> value.length() >= minLength;

局部变量存放在线程栈中,方法结束后栈帧就会退出;Lambda 对捕获值的生命周期可能更长。因此,编译器捕获的是变量的值,而不是让多个执行单元共享一个可变的栈变量。限制重新赋值也能避免“看起来在共享同一个变量,实际却不是”的语义混乱。

这个限制只针对局部变量。Lambda 仍然可以读取和修改对象字段,但并发场景下要自行保证线程安全。

⭐️Stream 和 Collection 有什么区别?

⭐️Stream 和 Collection 有什么区别?

Collection 主要负责保存和管理数据,Stream 主要负责声明对数据的计算过程。

Collection
Stream

Stream 本身不存储元素,数据通常来自集合、数组或 I/O 通道。

Stream 操作通常不会修改数据源,而是生成新的计算结果。

中间操作具有惰性,遇到终止操作后才会真正执行。

一个 Stream 通常只能消费一次,再次操作会抛出 IllegalStateException。

IllegalStateException

Stream 可以串行执行,也可以并行执行。

可以把集合看作数据结构,把 Stream 看作一条一次性的计算流水线。

Stream 的中间操作和终止操作有什么区别?

Stream 的中间操作和终止操作有什么区别?

中间操作返回新的 Stream,例如 filter()、map()、sorted()、distinct()。它们通常是惰性的,只负责记录要执行的步骤。

filter()
map()
sorted()
distinct()

终止操作会触发流水线计算,并得到最终结果或产生副作用,例如 collect()、reduce()、count()、forEach()。

collect()
reduce()
count()
forEach()
List<String> result = names.stream()
        .filter(name -> name.length() > 2)
        .map(String::toUpperCase)
        .collect(Collectors.toList());
List<String> result = names.stream()
        .filter(name -> name.length() > 2)
        .map(String::toUpperCase)
        .collect(Collectors.toList());

惰性执行使 Stream 有机会把多步操作融合起来,并利用 findFirst()、anyMatch()、limit() 等操作进行短路。不过,惰性不等于一定更快,最终效果仍取决于操作类型和数据规模。

findFirst()
anyMatch()
limit()

⭐️parallelStream 一定比普通 Stream 快吗?

⭐️parallelStream 一定比普通 Stream 快吗?

不一定。并行处理要承担任务拆分、线程调度和结果合并的成本,只有数据量足够大、计算可拆分、单个任务有一定计算量时,才可能获得明显收益。

使用 parallelStream() 时还要注意:

parallelStream()

默认使用 ForkJoinPool.commonPool(),可能和进程内其他并行任务争抢线程。

ForkJoinPool.commonPool()

不适合包含大量阻塞 I/O 的任务。

Lambda 中如果修改共享集合或共享变量,容易产生线程安全问题。

有顺序要求时,forEach() 和 forEachOrdered() 的语义、性能不同。

forEach()
forEachOrdered()

Web 服务中不要看到集合就直接改成并行流,应先用真实负载压测。

并行流更适合无副作用、CPU 密集、容易拆分的数据处理任务,不是一个通用的性能开关。

⭐️Optional 解决了什么问题?应该如何使用?

⭐️Optional 解决了什么问题?应该如何使用?

Optional<T> 用来明确表达“这里可能没有值”,让调用方显式处理空结果。它最适合作为方法返回值:

Optional<T>
Optional<User> findById(long id) {
    return Optional.ofNullable(userRepository.findById(id));
}
Optional<User> findById(long id) {
    return Optional.ofNullable(userRepository.findById(id));
}

常见用法包括 map()、flatMap()、filter()、orElseGet() 和 orElseThrow()。不建议无判断地调用 get(),否则只是把空指针风险换成了 NoSuchElementException。

map()
flatMap()
filter()
orElseGet()
orElseThrow()
get()
NoSuchElementException

orElse() 和 orElseGet() 也经常被追问:orElse() 的备用值会提前求值,orElseGet() 只在 Optional 为空时调用 Supplier。如果备用值的创建有开销或副作用,应优先考虑 orElseGet()。

orElse()
orElseGet()
orElse()
orElseGet()
orElseGet()

一般不建议把 Optional 用作实体字段、DTO 字段或方法参数。它的主要设计目标是返回值语义,而且 Optional 本身没有实现 Serializable,部分序列化框架和持久化框架对它的支持也不一致。

Optional
Serializable

接口默认方法解决了什么问题?发生冲突怎么办?

接口默认方法解决了什么问题?发生冲突怎么办?

默认方法允许接口新增带实现的方法,已有实现类不必立即修改,主要解决接口演进的兼容性问题。

多个默认方法发生冲突时,可以记住三条规则:

类或父类中已有的实例方法优先于接口默认方法。

更具体的子接口优先于父接口。

如果两个无继承关系的接口提供相同默认方法,实现类必须显式重写,并可通过 接口名.super.方法名() 选择调用哪个实现。

接口名.super.方法名()

接口有默认方法之后仍然不能替代抽象类。抽象类可以保存实例状态、定义构造方法并使用更丰富的访问控制;接口更适合表达能力和契约。

Java 8 日期时间 API 相比 Date 和 Calendar 好在哪里?

Java 8 日期时间 API 相比 Date 和 Calendar 好在哪里?

Java 8 引入的 java.time API 主要借鉴了 Joda-Time,常用类型包括 LocalDate、LocalTime、LocalDateTime、Instant、Duration、Period 和 ZonedDateTime。

java.time
LocalDate
LocalTime
LocalDateTime
Instant
Duration
Period
ZonedDateTime

它相对旧 API 的优势主要有:

核心类型不可变,天然更适合并发场景。

类型职责清晰,日期、时间、时间点、时区和时间间隔不再混在一个类里。

月份从 1 开始,API 更符合直觉。

DateTimeFormatter 是不可变且线程安全的,不需要像 SimpleDateFormat 那样额外处理并发问题。

DateTimeFormatter
SimpleDateFormat

涉及跨时区业务时,不要只保存 LocalDateTime。它不包含时区和 UTC 偏移,无法单独表示全球时间线上的唯一时刻;通常应根据业务选择 Instant、OffsetDateTime 或 ZonedDateTime。

LocalDateTime
Instant
OffsetDateTime
ZonedDateTime

CompletableFuture 相比 Future 有什么改进?

CompletableFuture 相比 Future 有什么改进?

传统 Future 主要提供提交任务、查询状态、取消任务和阻塞获取结果的能力,多个异步任务之间不容易组合。

Future

CompletableFuture 同时实现了 Future 和 CompletionStage,支持:

CompletableFuture
Future
CompletionStage

通过 thenApply()、thenCompose()、thenCombine() 等方法编排任务。

thenApply()
thenCompose()
thenCombine()

通过 exceptionally()、handle() 等方法处理异常。

exceptionally()
handle()

主动完成一个任务。

用回调继续处理结果,减少手写阻塞等待。

需要注意,名称带 Async 且没有显式传入 Executor 的方法通常使用 ForkJoinPool.commonPool()。生产环境中如果任务需要隔离、限流或包含阻塞操作,最好传入业务自己的线程池。

Async
ForkJoinPool.commonPool()

Java 9 到 Java 17

Java 9 到 Java 17

⭐️Java 9 模块系统解决了什么问题?

⭐️Java 9 模块系统解决了什么问题?

Java 平台模块系统(JPMS)把一组包、资源和模块描述符组织成显式模块,核心目标是可靠的依赖配置和更强的封装。

module com.example.order {
    requires com.example.user;
    exports com.example.order.api;
}
module com.example.order {
    requires com.example.user;
    exports com.example.order.api;
}

requires 声明当前模块依赖的模块。

requires

exports 允许其他模块访问指定包中的公共类型。

exports

opens 允许其他模块通过深反射访问指定包。

opens

requires transitive 可以把依赖继续暴露给使用当前模块的模块。

requires transitive

模块路径以整个模块为单位解析依赖,类路径则以类和资源为单位查找。JPMS 能在编译期和启动期更早发现缺失依赖,并可以配合 jlink 构建裁剪后的运行时镜像。

jlink

不过,JPMS 不负责选择第三方依赖版本,也不能替代 Maven 或 Gradle。

Java 9 的 List.of() 和 Arrays.asList() 有什么区别?

Java 9 的 List.of() 和 Arrays.asList() 有什么区别?

两者都能快速创建列表,但语义不同:

List.of() 返回不可修改列表,不能执行 add()、remove() 或 set(),并且不允许 null 元素。

List.of()
add()
remove()
set()
null

Arrays.asList() 返回数组的定长视图,不能增删元素,但可以通过 set() 修改元素。

Arrays.asList()
set()

Arrays.asList() 与原数组相互影响,修改其中一方的元素,另一方也能看到。

Arrays.asList()

同类的 Set.of() 和 Map.of() 除了不允许 null,还会拒绝重复元素或重复键。

Set.of()
Map.of()
null

⭐️var 是动态类型吗?有哪些限制?

⭐️var 是动态类型吗?有哪些限制?

var 不是动态类型,也不是 JavaScript 中的变量声明。Java 仍然是静态类型语言,编译器会根据初始化表达式推断出确定类型,之后不能把变量改成其他类型。

var
var names = new ArrayList<String>(); // 推断为 ArrayList<String>
names = new LinkedList<>();          // 编译错误
var names = new ArrayList<String>(); // 推断为 ArrayList<String>
names = new LinkedList<>();          // 编译错误

var 只能用于带初始化器的局部变量、循环变量等位置,不能用于字段、方法参数和方法返回类型。它也不能直接用 null、Lambda 或方法引用来初始化,因为这些表达式缺少独立的目标类型。

var
null

是否使用 var 要看可读性。右侧已经明确写出类型时通常能减少重复;如果类型需要读者跨多层调用才能判断,显式写出类型反而更清楚。

var

Java 11 的 HTTP Client 有什么特点?

Java 11 的 HTTP Client 有什么特点?

Java 11 将 java.net.http.HttpClient 标准化,提供同步和异步请求,支持 HTTP/1.1、HTTP/2 以及 WebSocket。异步请求返回 CompletableFuture,可以直接接入异步任务编排。

java.net.http.HttpClient
CompletableFuture
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com"))
        .GET()
        .build();

CompletableFuture<HttpResponse<String>> response = client.sendAsync(
        request,
        HttpResponse.BodyHandlers.ofString()
);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com"))
        .GET()
        .build();

CompletableFuture<HttpResponse<String>> response = client.sendAsync(
        request,
        HttpResponse.BodyHandlers.ofString()
);

它解决了 JDK 长期缺少现代 HTTP 客户端的问题,但在连接池细粒度配置、拦截器、声明式调用等方面,成熟的第三方客户端和框架仍然可能更合适。

⭐️Switch 表达式和传统 switch 有什么区别?

⭐️Switch 表达式和传统 switch 有什么区别?

Java 14 正式引入 Switch 表达式。它可以直接返回值,箭头标签默认不会贯穿到下一个 case,因此能减少漏写 break 的问题。

case
break
String type = switch (day) {
    case SATURDAY, SUNDAY -> "weekend";
    default -> "workday";
};
String type = switch (day) {
    case SATURDAY, SUNDAY -> "weekend";
    default -> "workday";
};

如果一个分支需要执行多条语句,可以使用代码块并通过 yield 返回结果:

yield
int score = switch (level) {
    case "A" -> 100;
    case "B" -> 80;
    default -> {
        log.warn("unknown level: {}", level);
        yield 0;
    }
};
int score = switch (level) {
    case "A" -> 100;
    case "B" -> 80;
    default -> {
        log.warn("unknown level: {}", level);
        yield 0;
    }
};

Switch 表达式必须覆盖所有可能输入,通常需要 default;对于覆盖全部枚举常量的情况,编译器可以判断其穷尽性。

default

文本块是什么?它是原始字符串吗?

文本块是什么?它是原始字符串吗?

文本块在 Java 15 正式转正,用三个双引号表示多行字符串,适合编写 JSON、SQL、HTML 等内容。

String json = """
        {
          "name": "JavaGuide",
          "type": "website"
        }
        """;
String json = """
        {
          "name": "JavaGuide",
          "type": "website"
        }
        """;

文本块最终仍然是 String。编译器会处理公共缩进、换行和转义字符,所以它不是完全不处理字符的原始字符串,也不直接提供字符串插值。动态内容可以结合 formatted() 等方法生成。

String
formatted()

⭐️Record 和普通类有什么区别?

⭐️Record 和普通类有什么区别?

Record 在 Java 16 正式转正,适合表示以数据为中心的透明载体:

record User(long id, String name) {
    User {
        if (id <= 0) {
            throw new IllegalArgumentException("id must be positive");
        }
    }
}
record User(long id, String name) {
    User {
        if (id <= 0) {
            throw new IllegalArgumentException("id must be positive");
        }
    }
}

编译器会根据组件列表生成:

private final 字段和同名访问器,例如 id(),而不是 getId()。

private final
id()
getId()

规范构造方法。

基于所有组件的 equals()、hashCode() 和 toString()。

equals()
hashCode()
toString()

Record 类隐式为 final,不能继承其他类,但可以实现接口,也可以声明方法、静态字段和静态方法。

final

Record 只保证组件引用不能被重新赋值,不保证对象深度不可变。 如果组件是可变集合,集合内容仍然可以变化。需要真正不可变时,应在构造阶段进行防御性复制,并避免把内部可变对象直接暴露出去。

Record 也不是所有 JavaBean 的替代品。JPA 实体、需要无参构造和可变属性的对象、依赖继承层次的领域模型,通常仍应使用普通类。

instanceof 模式匹配解决了什么问题?

instanceof 模式匹配解决了什么问题?

Java 16 之后,可以在完成类型判断的同时声明已经转换好的变量,不需要再写一次强制类型转换:

if (obj instanceof String value && !value.isBlank()) {
    System.out.println(value.length());
}
if (obj instanceof String value && !value.isBlank()) {
    System.out.println(value.length());
}

模式变量只在编译器能够确认匹配成功的作用域内可见。因此,上面可以在 && 右侧使用 value,但不能简单改成 || 后仍在右侧使用它。

&&
value
||

⭐️什么是密封类?

⭐️什么是密封类?

密封类和密封接口可以明确限制哪些类型能够直接继承或实现自己,适合描述取值范围封闭的领域模型。

sealed interface Result permits Success, Failure {
}

record Success(String data) implements Result {
}

final class Failure implements Result {
}
sealed interface Result permits Success, Failure {
}

record Success(String data) implements Result {
}

final class Failure implements Result {
}

被允许的直接子类型必须继续声明为以下三种之一:

final:不能再被继承。

final

sealed:继续限制自己的子类型。

sealed

non-sealed:从这里重新开放继承。

non-sealed

Record 隐式为 final,因此可以直接作为密封接口的实现。密封层次与 Switch 模式匹配配合时,编译器可以检查分支是否穷尽,这比一长串容易遗漏的 instanceof 判断更安全。

final
instanceof

Java 21 重点新特性

Java 21 重点新特性

⭐️什么是虚拟线程?和平台线程有什么区别?

⭐️什么是虚拟线程?和平台线程有什么区别?

虚拟线程是 JDK 实现的轻量级 Thread,在 Java 21 正式转正。平台线程通常与操作系统线程一一对应;大量虚拟线程则由 JVM 调度到少量平台线程上执行。

Thread

当虚拟线程执行受支持的阻塞操作时,JVM 可以暂停虚拟线程并释放承载它的平台线程,让平台线程去执行其他虚拟线程。这样既保留了“一请求一线程”的同步编程方式,又能承载更高的并发量。

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<String> future = executor.submit(() -> loadFromRemote());
    System.out.println(future.get());
}
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<String> future = executor.submit(() -> loadFromRemote());
    System.out.println(future.get());
}

虚拟线程主要降低的是线程的创建、切换和阻塞成本,不会让 CPU 计算本身变快。CPU 密集型任务最终仍受 CPU 核心数限制。

虚拟线程适合哪些场景?

虚拟线程适合哪些场景?

虚拟线程适合任务数量很多、任务相互独立,而且大部分时间在等待网络、数据库或消息系统的 I/O 场景,例如请求处理、RPC 调用和 JDBC 查询。

下面几类场景不能因为换成虚拟线程就自然获得收益:

纯 CPU 密集型计算。

下游数据库连接池、HTTP 服务容量已经成为瓶颈。

大量依赖本地方法,并且会长时间固定承载线程的代码。

通过全局锁串行化的任务。

虚拟线程提高的是应用等待期间的线程利用率,不能替代数据库连接池、限流、熔断和背压。

⭐️虚拟线程还需要线程池吗?

⭐️虚拟线程还需要线程池吗?

不要为了复用线程而池化虚拟线程。虚拟线程创建成本很低,官方推荐为每个任务创建一个虚拟线程,newVirtualThreadPerTaskExecutor() 也是每个任务新建一个虚拟线程。

newVirtualThreadPerTaskExecutor()

如果业务只能允许 100 个请求同时访问下游,不应该把虚拟线程池大小设成 100,而应该用 Semaphore 等机制限制对应资源的并发量:

Semaphore
Semaphore permits = new Semaphore(100);

void callRemote() throws InterruptedException {
    permits.acquire();
    try {
        remoteClient.call();
    } finally {
        permits.release();
    }
}
Semaphore permits = new Semaphore(100);

void callRemote() throws InterruptedException {
    permits.acquire();
    try {
        remoteClient.call();
    } finally {
        permits.release();
    }
}

也就是说,平台线程池常把“线程复用”和“并发限制”绑在一起;使用虚拟线程后,这两个职责应当分开。

synchronized 会固定虚拟线程吗?

synchronized 会固定虚拟线程吗?

这个问题要区分 JDK 版本:

在 Java 21 到 Java 23 中,虚拟线程在 synchronized 代码块或方法内执行阻塞操作时,可能被固定在承载线程上。长时间、大量固定会削弱扩展性。

synchronized

Java 24 的 JEP 491 改造了 JVM 的监视器实现,已经消除几乎所有由 synchronized 导致的固定,既有代码通常无需为了虚拟线程专门改成 ReentrantLock。

synchronized
ReentrantLock

本地方法和 Foreign Function & Memory API 调用等少数场景仍可能固定虚拟线程。

因此,“虚拟线程中一律不能使用 synchronized”是过时且过度简化的结论。生产排查时应结合实际 JDK 版本、JFR 和线程转储观察,而不是只凭语法判断。

synchronized

虚拟线程可以使用 ThreadLocal 吗?

虚拟线程可以使用 ThreadLocal 吗?

可以。Java 21 正式版虚拟线程始终支持线程局部变量,现有依赖 ThreadLocal 的库通常可以继续运行。

ThreadLocal

问题在于规模:如果创建几十万甚至上百万个虚拟线程,并且每个线程都保存较大的 ThreadLocal 数据,内存成本会被同步放大。线程池时代“任务结束后必须 remove,避免值在线程复用期间残留”的问题,在短生命周期虚拟线程中有所减弱,但高基数和大对象占用仍然需要警惕。

对于请求 ID、用户身份等由调用者传给下游、在作用域内只读的上下文,可以考虑 Java 25 转正的 ScopedValue。

ScopedValue

⭐️Java 21 的 Record 模式和 Switch 模式匹配有什么用?

⭐️Java 21 的 Record 模式和 Switch 模式匹配有什么用?

Record 模式可以直接解构 Record,Switch 模式匹配可以根据类型选择分支。二者和密封类组合后,特别适合表达封闭的数据模型:

sealed interface Shape permits Circle, Rectangle {
}

record Circle(double radius) implements Shape {
}

record Rectangle(double width, double height) implements Shape {
}

static double area(Shape shape) {
    return switch (shape) {
        case Circle(double radius) -> Math.PI * radius * radius;
        case Rectangle(double width, double height) -> width * height;
    };
}
sealed interface Shape permits Circle, Rectangle {
}

record Circle(double radius) implements Shape {
}

record Rectangle(double width, double height) implements Shape {
}

static double area(Shape shape) {
    return switch (shape) {
        case Circle(double radius) -> Math.PI * radius * radius;
        case Rectangle(double width, double height) -> width * height;
    };
}

这里不需要显式强制类型转换,也不需要 default。因为 Shape 是密封接口,而且两个允许的子类型都已经覆盖,编译器能够证明分支是穷尽的。

default
Shape

Sequenced Collections 解决了什么问题?

Sequenced Collections 解决了什么问题?

Java 21 新增 SequencedCollection、SequencedSet 和 SequencedMap,统一表示具有确定遍历顺序的集合,并提供首尾访问和反向视图能力。

SequencedCollection
SequencedSet
SequencedMap
SequencedCollection<String> names = new ArrayList<>();
names.addFirst("Java");
names.addLast("Guide");

String first = names.getFirst();
SequencedCollection<String> reversed = names.reversed();
SequencedCollection<String> names = new ArrayList<>();
names.addFirst("Java");
names.addLast("Guide");

String first = names.getFirst();
SequencedCollection<String> reversed = names.reversed();

过去 List、Deque、LinkedHashSet、SortedSet 等有序集合的首尾操作并不统一,有些还缺少方便的反向遍历 API。新接口把这些能力纳入同一套类型体系。

List
Deque
LinkedHashSet
SortedSet

reversed() 返回的是反向顺序视图,不是无关联的新副本。原集合发生变化会反映到视图中,视图允许修改时也会写回原集合。

reversed()

Java 21 的分代 ZGC 是什么?

Java 21 的分代 ZGC 是什么?

Java 21 引入分代 ZGC,把堆划分为年轻代和老年代,利用“大多数对象朝生夕死”的弱分代假说,减少每次收集都扫描全部存活对象带来的成本。

ZGC 仍然以低暂停为主要目标,分代模式主要是进一步提高应用吞吐量、减少内存和 CPU 开销。它不意味着任何应用换用后都会更快,GC 选择仍然要根据堆大小、延迟目标、吞吐目标和真实压测结果决定。

Java 25、Java 26 与特性状态

Java 25、Java 26 与特性状态

⭐️ScopedValue 和 ThreadLocal 有什么区别?

⭐️ScopedValue 和 ThreadLocal 有什么区别?

ScopedValue 在 Java 25 正式转正,用于把不可变上下文从调用者传递给同一线程内的直接或间接被调用方法,也可以与结构化并发配合传给子任务。

ScopedValue
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

void handle(String requestId) {
    ScopedValue.where(REQUEST_ID, requestId)
            .run(() -> service());
}

void service() {
    log.info("requestId={}", REQUEST_ID.get());
}
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

void handle(String requestId) {
    ScopedValue.where(REQUEST_ID, requestId)
            .run(() -> service());
}

void service() {
    log.info("requestId={}", REQUEST_ID.get());
}

二者的主要区别是:

对比项ThreadLocalScopedValue数据变化可反复 set() 和 remove()绑定后在作用域内不可变生命周期容易扩散到整个线程生命周期由 run() 或 call() 的词法作用域界定数据流向任意下游代码都可能修改由调用者向被调用者单向传递虚拟线程成本大量线程各自保存值时成本可能较高针对大量线程共享只读上下文做了优化

对比项ThreadLocalScopedValue

对比项

ThreadLocal

ThreadLocal

ScopedValue

ScopedValue

数据变化可反复 set() 和 remove()绑定后在作用域内不可变

数据变化

可反复 set() 和 remove()

set()
remove()

绑定后在作用域内不可变

生命周期容易扩散到整个线程生命周期由 run() 或 call() 的词法作用域界定

生命周期

容易扩散到整个线程生命周期

由 run() 或 call() 的词法作用域界定

run()
call()

数据流向任意下游代码都可能修改由调用者向被调用者单向传递

数据流向

任意下游代码都可能修改

由调用者向被调用者单向传递

虚拟线程成本大量线程各自保存值时成本可能较高针对大量线程共享只读上下文做了优化

虚拟线程成本

大量线程各自保存值时成本可能较高

针对大量线程共享只读上下文做了优化

ScopedValue 不是 ThreadLocal 的全面替代品。需要在线程内累积和更新状态时,ThreadLocal 仍有用;传递只读请求上下文时,ScopedValue 通常更容易推理。

ScopedValue

Java 25 还有哪些值得了解的正式特性?

Java 25 还有哪些值得了解的正式特性?

Java 25 的常规面试频率目前不高,先记住下面几项即可:

紧凑源文件与实例 main 方法:小程序可以省略显式类声明,main 也不再必须是 public static void main(String[] args)。它主要降低入门和编写小型程序的门槛,并不要求企业项目改写入口类。

main
main
public static void main(String[] args)

模块导入声明:import module java.base; 可以按模块批量导入其导出包中的公共类型。它只影响源码中的名称导入,不等于在 module-info.java 中声明 requires 依赖。

import module java.base;
module-info.java
requires

灵活的构造函数体:允许在 super(...) 或 this(...) 之前校验参数、计算参数以及初始化当前类的字段,但此时不能读取正在构造的实例。

super(...)
this(...)

紧凑对象头:HotSpot 可以把常见 64 位对象头压缩到 64 位,减少堆占用;Java 25 中仍需通过 -XX:+UseCompactObjectHeaders 显式启用。

-XX:+UseCompactObjectHeaders

分代 Shenandoah:在 Shenandoah 的低暂停目标上引入分代回收,以改善吞吐量和资源开销,同样需要结合发行版支持与实际负载评估。

Java 26 有哪些值得了解的正式特性?

Java 26 有哪些值得了解的正式特性?

Java 26 是非 LTS 版本,面试中优先掌握下面几项正式特性即可:

HTTP Client 支持 HTTP/3:java.net.http.HttpClient 正式支持基于 QUIC 的 HTTP/3。默认首选协议仍是 HTTP/2,需要使用时应显式指定 HTTP_3;服务端不支持时可以回退到 HTTP/2 或 HTTP/1.1。

java.net.http.HttpClient
HTTP_3

G1 吞吐量优化:通过双卡表等机制减少应用线程写屏障与 GC 线程之间的同步开销,主要改善 G1 的吞吐量。

AOT 对象缓存支持任意 GC:Project Leyden 的 AOT 对象缓存不再只支持 G1,也可以与 ZGC 等垃圾收集器配合,用于改善应用启动和预热性能。

准备让 final 真正不可变:通过深度反射修改 final 字段时会发出警告,为未来默认限制这种行为做准备。

final
final

移除 Applet API:已经长期废弃、现代浏览器也不再支持的 Applet API 被正式移除。

结构化并发、延迟常量、基本类型模式匹配和 PEM 编解码在 Java 26 中仍是预览特性,Vector API 仍是孵化特性。它们随 JDK 发布,不代表 API 已经稳定转正。

正式特性、预览特性、孵化特性和实验特性有什么区别?

正式特性、预览特性、孵化特性和实验特性有什么区别?

类型含义常见启用方式正式特性已经成为 Java SE 或 JDK 的长期正式能力默认可用预览特性规格和实现较完整,但仍可能修改或移除编译和运行都需要 --enable-preview孵化特性尚未达到标准化程度的 API 或工具通常位于 jdk.incubator.* 模块,需要显式添加模块实验特性HotSpot 等实现中的早期功能,不属于 Java SE 标准能力常需要 -XX:+UnlockExperimentalVMOptions 等参数

类型含义常见启用方式

类型

含义

常见启用方式

正式特性已经成为 Java SE 或 JDK 的长期正式能力默认可用

正式特性

已经成为 Java SE 或 JDK 的长期正式能力

默认可用

预览特性规格和实现较完整,但仍可能修改或移除编译和运行都需要 --enable-preview

预览特性

规格和实现较完整,但仍可能修改或移除

编译和运行都需要 --enable-preview

--enable-preview

孵化特性尚未达到标准化程度的 API 或工具通常位于 jdk.incubator.* 模块,需要显式添加模块

孵化特性

尚未达到标准化程度的 API 或工具

通常位于 jdk.incubator.* 模块,需要显式添加模块

jdk.incubator.*

实验特性HotSpot 等实现中的早期功能,不属于 Java SE 标准能力常需要 -XX:+UnlockExperimentalVMOptions 等参数

实验特性

HotSpot 等实现中的早期功能,不属于 Java SE 标准能力

常需要 -XX:+UnlockExperimentalVMOptions 等参数

-XX:+UnlockExperimentalVMOptions

例如,Java 25 中 ScopedValue 已是正式 API,结构化并发仍处于预览阶段,Vector API 仍处于孵化阶段。不能因为某项特性已经随 JDK 发布,就默认它的 API 和行为不会变化。

ScopedValue

预览特性的典型启用方式如下:

javac --enable-preview --release 25 Main.java
java --enable-preview Main
javac --enable-preview --release 25 Main.java
java --enable-preview Main

使用某个版本预览特性编译出来的代码,通常需要在同版本 JDK 上启用预览能力运行,不适合直接作为稳定公共库广泛分发。

JDK 升级常见问题

JDK 升级常见问题

⭐️从 Java 8 升级到 Java 17 或 Java 21,需要注意什么?

⭐️从 Java 8 升级到 Java 17 或 Java 21,需要注意什么?

升级 JDK 不只是修改一个版本号,至少要检查下面几类问题:

先升级依赖和构建工具:确认 Spring、ORM、字节码工具、测试框架、Maven/Gradle 插件和 Java Agent 支持目标 JDK。

排查 JDK 内部 API 和深反射:Java 17 对 JDK 内部 API 实施更强封装,依赖 sun.* 或反射访问 JDK 私有成员的旧代码可能失败。--add-opens 可以作为迁移期手段,但不应成为永久方案。

sun.*
--add-opens

检查已移除的模块和工具:Java 11 移除了 Java EE、CORBA 等模块,旧项目如果依赖 JAXB 等 API,需要显式引入替代依赖。

重新验证 JVM 参数和 GC:旧参数可能已废弃或移除,默认 GC 和日志参数也发生过变化。不要直接复制 Java 8 的启动脚本。

做兼容性和性能测试:覆盖编译、单元测试、集成测试、启动、序列化、反射、TLS、数据库驱动、监控 Agent 和关键性能指标。

分阶段上线:先在 CI 和预发环境运行,再灰度到少量实例,观察错误率、延迟、吞吐量、CPU、内存和 GC。

另外,javax. 到 jakarta. 主要是 Jakarta EE 和相关框架的生态迁移,不是 Java 17 本身把所有 javax 包改了名。它经常与 Spring Boot 3、JDK 17 升级一起发生,但面试时不要混为一谈。

javax.*
jakarta.*
javax

javac 的 --release 和 -source、-target 有什么区别?

javac 的 --release 和 -source、-target 有什么区别?

-source 控制允许使用的 Java 源码语法,-target 控制生成的 class 文件版本。只设置这两个参数时,编译器仍可能看到当前 JDK 中新增的标准库 API,导致代码在编译时通过,放到旧 JDK 上却出现 NoSuchMethodError 或 ClassNotFoundException。

-source
-target
NoSuchMethodError
ClassNotFoundException

--release 会同时约束语言版本、class 文件版本以及目标 Java SE 版本可用的标准 API,更适合编译需要运行在旧 JDK 上的代码:

--release
javac --release 17 Main.java
javac --release 17 Main.java

它解决的是 JDK 标准 API 的兼容性检查,第三方依赖是否支持目标运行时仍要由构建配置、依赖管理和测试共同保证。

面试中如何回答“你项目里用过哪些 Java 新特性”?

面试中如何回答“你项目里用过哪些 Java 新特性”?

不要把版本发布说明背一遍,最好围绕真实代码回答:

说清楚使用了什么,例如 Stream、CompletableFuture、Record 或虚拟线程。

说明它解决了原来什么问题,代码或并发模型发生了什么变化。

补充限制和踩坑,例如并行流共用线程池、Record 只是浅不可变、虚拟线程不适合 CPU 密集任务。

最后给出效果,最好是可验证的数据或维护性变化。

如果项目仍在 Java 8,也可以如实回答没有在生产环境使用 Java 21,同时说明自己如何评估升级:先处理框架和依赖兼容,再做测试与灰度。比起只报一串新特性名称,这种回答更能体现工程判断。

参考资料

参考资料

Oracle Java SE Support Roadmap

Oracle Java SE Support Roadmap

JEP 261:Module System

JEP 261:Module System

JEP 286:Local-Variable Type Inference

JEP 286:Local-Variable Type Inference

JEP 395:Records

JEP 395:Records

JEP 409:Sealed Classes

JEP 409:Sealed Classes

JEP 431:Sequenced Collections

JEP 431:Sequenced Collections

JEP 440:Record Patterns

JEP 440:Record Patterns

JEP 441:Pattern Matching for switch

JEP 441:Pattern Matching for switch

JEP 444:Virtual Threads

JEP 444:Virtual Threads

JEP 491:Synchronize Virtual Threads without Pinning

JEP 491:Synchronize Virtual Threads without Pinning

JEP 506:Scoped Values

JEP 506:Scoped Values

JEP 12:Preview Features

JEP 12:Preview Features

JEP 11:Incubator Modules

JEP 11:Incubator Modules

写在最后

写在最后

感谢你能看到这里,也希望这篇文章对你有点用。

JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):GitHub | Gitee。

GitHub

Gitee

如果你想要付费支持/面试辅导(比如简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!

知识星球

JavaGuide 公众号