目录
原始类型在模式匹配、instanceof和switch中的使用
Java Development Kit (JDK) 23,作为Java语言的重要更新,于2024年9月17日正式发布,为开发者带来了多项新特性和改进。这些新特性不仅提高了Java的性能和可用性,还为开发者提供了更多创新的工具和方法。以下是JDK 23的一些关键新特性及其详细解析。
模块导入声明(JEP 476)
一、模块导入声明概述
JDK 23引入了模块导入声明(Module Import Declarations),这一特性简化了模块化库的重用。开发者可以更简洁地导入整个模块,而无需逐一声明每个包。这不仅有助于简化代码,还使得初学者更容易使用第三方库和Java基础类。
二、模块导入声明的工作原理
- 自动导入模块导出的公开顶层类和接口:使用模块导入声明后,Java编译器会自动导入指定模块导出的所有公开顶层类和接口。这意味着开发者无需再为模块中的每个类单独编写import语句。
- 传递依赖导入:导入一个模块时,如果该模块依赖于其他模块,则这些依赖模块也会被自动导入。这进一步简化了模块的依赖管理。
- 避免类型冲突:由于模块导入声明会导入多个包,因此可能会出现类型冲突的情况。例如,如果两个模块都包含相同名称的类,直接使用该类名可能会导致编译错误。为了解决这个问题,开发者可以显式地导入所需的类型,以消除歧义。
三、模块导入声明的使用场景
模块导入声明适用于以下场景:
- 项目开发早期阶段:在项目开发的早期阶段,开发者可能需要频繁地导入和使用模块中的多个类。此时,使用模块导入声明可以显著简化代码编写过程。
- 原型开发:在开发应用原型时,快速迭代和简化代码是关键。模块导入声明有助于开发者更快速地构建和测试原型。
- API探索:当开发者正在探索新API的使用时,模块导入声明可以让他们更快速地访问和使用模块中的类和接口。
四、模块导入声明的示例
以下是一个使用模块导入声明的示例:
// 导入java.base模块
import module java.base;
public class ModuleImportExample {
public static void main(String[] args) {
// 使用java.base模块中的类
java.util.List<String> list = new java.util.ArrayList<>();
list.add("Hello");
list.add("World");
System.out.println(list);
// 使用java.base模块中的接口
java.util.function.Function<String, Integer> lengthFunction = String::length;
System.out.println(lengthFunction.apply("Hello, World!"));
}
}
在这个示例中,通过import module java.base;语句导入了java.base模块。然后,在代码中可以直接使用java.base模块中的List类和Function接口,而无需为每个类单独编写import语句。
五、注意事项
- 预览特性:模块导入声明目前是JDK 23的预览特性。因此,在编译和运行时需要添加--enable-preview参数。
- 不推荐在生产代码中使用:虽然模块导入声明可以简化代码编写过程,但由于它会导入大量的包和类,可能会导致类型冲突和编译错误。因此,在生产代码中通常不推荐使用模块导入声明。相反,建议只导入所需的单个类型,以提高代码的可读性和可维护性。
- 显式导入优先:当模块导入声明与显式导入发生冲突时,显式导入将覆盖模块导入声明。因此,在需要解决类型冲突时,可以使用显式导入来指定所需的类型。
原始类型在模式、instanceof和switch中的使用
详细描述。
原始类型在模式匹配、instanceof和switch中的使用
1. 原始类型在模式匹配中的使用
在JDK 23之前,模式匹配通常用于引用类型。但在JDK 23中,原始类型(如int、long、boolean等)也可以直接在模式匹配结构中使用。这意味着开发者无需对值进行不必要的装箱和拆箱操作,从而提高了代码的效率。
2. 原始类型在instanceof中的使用
instanceof是Java中用于检查对象类型的运算符。在JDK 23中,instanceof运算符得到了扩展,可以与原始类型无缝协作。这意味着开发者可以直接使用原始类型进行类型检查,而无需先将它们转换为包装类型。
3. 原始类型在switch中的使用
switch表达式在Java中用于基于不同条件执行不同代码块。在JDK 23中,switch表达式也支持了原始类型。这意味着开发者可以直接在switch表达式中使用原始类型作为case标签,从而简化了代码编写。
示例代码
以下是一个使用原始类型在模式匹配、instanceof和switch中的示例代码:
// 定义一个包含ID和登录状态的User记录类型
record User(long id, boolean loggedIn) {}
// 定义一个枚举类型表示订单状态
public enum OrderStatus {
NEW, PROCESSING, DISPATCHED, DELIVERED
}
// 定义一个方法,根据用户登录状态和订单状态打印相应的消息
public class OrderService {
public static void main(String[] args) {
var user1 = new User(12345L, true); // 已登录用户
var user2 = new User(0L, false); // 未登录用户
// 使用switch表达式和instanceof检查用户登录状态
startProcessing(OrderStatus.NEW, switch (user1.loggedIn()) {
case true -> user1.id();
case false -> {
System.out.println("Unrecognized user. Unable to process order.");
yield -1L; // 使用原始类型long作为case标签
}
});
// 使用switch表达式和instanceof检查用户登录状态
startProcessing(OrderStatus.NEW, switch (user2.loggedIn()) {
case true -> user2.id();
case false -> {
System.out.println("Unrecognized user. Unable to process order.");
yield -1L; // 使用原始类型long作为case标签
}
});
}
// 根据用户ID和订单状态打印消息
public static void startProcessing(OrderStatus orderStatus, long userId) {
// 使用switch表达式和原始类型case标签处理不同的订单状态
switch (orderStatus) {
case NEW -> System.out.println("Order for User " + userId + " is received and will start processing");
case PROCESSING -> System.out.println("Order for User " + userId + " is being processed");
case DISPATCHED, DELIVERED -> System.out.println("Order for User " + userId + " has already been processed");
}
}
}
在这个示例中,我们定义了一个User记录类型和一个OrderStatus枚举类型。然后,在OrderService类的main方法中,我们使用switch表达式和instanceof运算符来检查用户的登录状态,并根据登录状态和订单状态打印相应的消息。注意,在switch表达式中,我们直接使用了原始类型long和boolean作为case标签,从而简化了代码编写。
灵活的构造函数体
详细描述
在传统的Java中,构造函数的第一条语句必须是调用另一个构造函数(使用this())或父类的构造函数(使用super())。这一限制确保了对象在构造过程中按照特定的顺序进行初始化。然而,在某些情况下,开发者可能希望在调用super()或this()之前执行一些初始化逻辑,例如验证参数或初始化某些字段。
在JDK 23中,灵活的构造函数体特性打破了这一限制。它允许在显式调用构造函数之前放置语句,但这些语句不能引用正在构造的实例(即不能访问this引用的字段或方法)。这些前置语句可以用于初始化类的静态字段、执行参数验证或其他与实例初始化不直接相关的逻辑。
示例
以下是一个使用灵活构造函数体的示例:
public class Animal {
Animal() {
overriddenMethod();
}
void overriddenMethod() {
System.out.println("Hello from Animal");
}
}
public class Dog extends Animal {
final Integer x;
Dog(int x) {
// 在调用super()之前初始化x
this.x = x;
if (x <= 0) {
throw new IllegalArgumentException("x must be positive");
}
super(); // 调用父类的构造函数
}
@Override
void overriddenMethod() {
System.out.println("Hello from Dog with x = " + x);
}
public static void main(String[] args) {
try {
new Dog(5); // 输出: Hello from Dog with x = 5
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
try {
new Dog(-1); // 输出: x must be positive
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
在这个示例中,Dog类的构造函数在调用super()之前初始化了x字段,并进行了参数验证。如果x小于或等于0,则抛出IllegalArgumentException。这种初始化顺序在传统的Java中是不允许的,但在JDK 23中,由于灵活的构造函数体特性,它变得可行。
注意事项
- 前置语句不能引用正在构造的实例(即不能访问this引用的字段或方法)。如果需要在构造函数中访问实例字段或方法,则必须在调用super()或this()之后进行。
- 灵活的构造函数体特性提高了代码的灵活性和可读性,但开发者需要谨慎使用,以确保对象的正确初始化和线程安全。
隐式声明的类和实例主方法
描述
- 隐式声明类:
- 在JDK 23之前,定义一个类需要显式地使用class关键字,并指定类名。
- JDK 23引入了隐式声明类的概念,即在不显式声明类名的情况下,编译器会自动为该类生成一个默认类名(通常是一个匿名类)。
- 简化main方法声明:
- 在JDK 23之前,main方法是Java程序的入口点,必须被声明为public static void main(String[] args)。
- JDK 23简化了main方法的声明,允许开发者省略public、static关键字,甚至方法参数。这意味着main方法不再需要显式声明为静态方法,也不再强制要求接受字符串数组作为参数。
示例
以下是一个使用隐式声明的类和实例主方法的示例:
// 传统方式定义main方法
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
// 使用JDK 23的隐式声明类和实例主方法
void main() {
System.out.println("Hello, World!");
}
在上面的示例中,第二个main方法定义省略了类声明、public、static关键字以及方法参数。编译器会自动处理这些细节,使得代码更加简洁。
注意事项
- 当使用隐式声明的类和实例主方法时,编译器会自动为该类生成一个默认类名,因此该类不能在源代码中按名称引用。
- 隐式声明的类继承自Object类,并且不实现任何接口。
- 隐式声明的实例主方法不再强制要求它们是static或public的,并且不带参数的方法也可以作为有效的程序入口点。
Markdown文档注释
Markdown文档注释的引入背景
在Java生态系统中,Javadoc是生成项目API文档的重要工具。然而,传统的Javadoc注释需要使用HTML格式来编写,这对于开发者来说不仅繁琐,而且不利于阅读和维护。为了解决这一问题,JDK 23引入了Markdown文档注释,允许开发者使用更简洁、易读的Markdown语法来编写Javadoc注释。
Markdown文档注释的优势
- 简洁性:Markdown语法比HTML更加简洁,减少了不必要的标签和属性,使得注释内容更加清晰。
- 易读性:Markdown语法支持结构化文本,如标题、列表、代码块等,使得注释内容更加易于阅读和理解。
- 跨平台兼容性:Markdown语法具有广泛的兼容性,可以在不同的平台和编辑器中保持一致的效果。
Markdown文档注释的示例
下面是一个使用Markdown语法编写的Javadoc注释示例:
/**
* # Calculator 类
*
* 这是一个简单的计算器类,用于执行基本的数学运算。
*
* 功能:
* - 加法
* - 减法
*
* # 代码示例:
* Calculator calculator = new Calculator();
* int result = calculator.add(5, 3);
* System.out.println(result); // 输出:8
* ```
*/
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}
在这个示例中,使用了Markdown的标题(#)、列表(-)和代码块(```java)等语法来结构化注释内容。这使得注释更加清晰、易读,并且易于维护。
Markdown文档注释的注意事项
- 注释格式:Markdown文档注释需要使用三个斜杠(///)来开始每一行,而不是传统的两个斜杠(//)。这是为了区分普通的单行注释和Markdown文档注释。
- HTML兼容性:虽然Markdown文档注释主要使用Markdown语法,但在某些情况下仍然可以使用HTML标签来补充或扩展Markdown的功能。例如,可以使用HTML的<u>标签来添加下划线文本。
- 工具支持:为了充分利用Markdown文档注释的优势,建议使用支持Markdown语法的IDE或编辑器来编写和阅读Javadoc注释。
向量API
向量API概述
- 目标:向量API的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。它旨在支持x64和AArch64架构,并与Valhalla项目保持一致,后者旨在增强Java对象模型的价值对象。
- 优势:通过利用现代CPU的SIMD(单指令多数据)指令集,向量API可以显著提高数值计算的性能。与传统的标量计算相比,向量计算能够同时处理多个数据元素,从而减少了指令的执行次数和数据传输的开销。
向量API的关键组件
- VectorSpecies:表示向量的类型和大小。它是向量API中的核心类之一,用于指定向量的元素类型和长度。
- Vector:表示向量数据。它是VectorSpecies的实例化对象,可以包含特定类型和长度的数据元素。
- 操作:向量API提供了一系列的操作,如加法、减法、乘法、除法等,这些操作可以直接应用于向量对象上,从而实现高效的向量计算。
示例代码
以下是一个使用向量API进行向量计算的示例代码:
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
public class VectorAPIDemo {
// 定义一个静态常量,表示首选的Float向量类型
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
// 向量计算方法
public static void vectorComputation(float[] a, float[] b, float[] c) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
// 使用向量API进行向量计算
for (; i < upperBound; i += SPECIES.length()) {
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
var vc = va.mul(va).add(vb.mul(vb)).neg();
vc.intoArray(c, i);
}
// 处理剩余的元素(如果向量长度不是数组长度的整数倍)
for (; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
public static void main(String[] args) {
float[] a = {1.0f, 2.0f, 3.0f, 4.0f};
float[] b = {5.0f, 6.0f, 7.0f, 8.0f};
float[] c = new float[a.length];
// 调用向量计算方法
vectorComputation(a, b, c);
// 打印结果
for (float value : c) {
System.out.println(value);
}
}
}
代码说明
- 初始化向量类型:通过FloatVector.SPECIES_PREFERRED获取首选的Float向量类型,它表示当前平台上性能最佳的Float向量类型。
- 向量计算:在vectorComputation方法中,首先计算向量计算的循环上界upperBound,然后使用向量API进行向量计算。在循环中,通过FloatVector.fromArray方法将数组中的元素加载到向量中,然后进行向量的乘法、加法和取反操作,最后将结果存储回数组中。
- 处理剩余元素:如果向量长度不是数组长度的整数倍,则在循环结束后处理剩余的元素。
- 打印结果:在main方法中,初始化输入数组并调用vectorComputation方法进行计算,最后打印结果。
流收集器
流收集器的工作原理
流收集器是Java流API中的一个关键组件,它负责将流中的元素收集到一个汇总结果中。在使用流的一些中间操作处理完流中的元素后,一般要使用一个终止操作来返回处理后的结果,而收集器就是最常见的终止操作之一。收集器通过定义一系列的操作(如累加、合并、转换等)来将流中的元素汇聚到一个可变的结果容器中,并在所有元素处理完毕后,将结果容器转换为最终要返回的对象。
流收集器的组成
一个收集器通常由以下几个部分组成:
- Supplier:提供中间结果容器的供应者。它返回一个空的结果容器,用于存储流中的元素。
- Accumulator:累加器。它定义了如何将流中的元素累加到中间结果容器中。
- Combiner:合并器。在多线程环境下,当每个线程产生各自的结果容器时,合并器负责将这些容器合并成一个。
- Finisher:转换器。它将中间结果容器转换为最终要返回的对象。
流收集器的示例
以下是一个使用流收集器将流中的元素收集到一个列表中的示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamCollectorExample {
public static void main(String[] args) {
// 创建一个包含字符串的数组
String[] strings = {"apple", "banana", "cherry"};
// 将数组转换为流,并使用collect方法将流中的元素收集到一个列表中
List<String> collectedList = Arrays.stream(strings)
.collect(Collectors.toList());
// 输出收集到的列表
System.out.println(collectedList);
}
}
在这个示例中,Collectors.toList()返回了一个收集器实例,它使用了ArrayList作为中间结果容器。累加器将流中的每个元素添加到ArrayList中,而转换器则直接将ArrayList转换为最终要返回的List对象。
流收集器的自定义
除了使用Java提供的内置收集器(如Collectors.toList()、Collectors.toSet()等)外,开发者还可以根据需要自定义收集器。自定义收集器需要实现Collector接口,并提供该接口要求的所有方法。
类文件API
一、类文件API概述
类文件API(JEP 466)提供了解析、生成和转换Java类文件的标准API。这一API的引入是为了解决Java生态系统中对类文件处理能力的依赖问题。传统的框架通常捆绑了类文件库(如ASM或Javassist)来处理类文件,但这些库可能无法及时跟上JDK的发布节奏和类文件格式的发展。类文件API旨在提供一个与JDK版本保持同步的标准化解决方案。
二、类文件API的主要组件
类文件API位于java.lang.classfile包中,主要由三个主要组件组成:
- 元素(Elements):描述类文件部分的不可变描述,如指令、属性、字段、方法或整个文件。
- 构建器(Builders):对应的构建器用于复合元素,提供特定的构建方法(例如,ClassBuilder::withMethod)并作为元素类型的消费者。
- 转换(Transformations):接受一个元素和一个构建器的函数,确定元素是否以及如何转换为其他元素。这允许灵活地修改类文件元素。
三、类文件API的改进
与JDK 22的预览版本相比,JDK 23中的类文件API进行了一些重要改进:
- 简化的CodeBuilder:CodeBuilder类已被简化,提供了三种类型的字节码指令工厂方法:低级工厂、中级工厂和基本块的高级构建器。根据反馈,删除了重复的低级方法或不经常使用的中级方法,并将剩余的中级方法重命名以提高可用性。
- 更准确的超类和超接口建模:Attributes中的AttributeMapper实例已经通过静态方法而不是静态字段变得可访问,以允许延迟初始化并减少启动成本。同时,Signature.TypeArg被重新设计为代数数据类型,以便于在TypeArg的种类为有界时访问绑定的类型。
- 类型感知的方法:添加了类型感知的ClassReader::readEntryOrNull和ConstantPool::entryByIndex方法。如果索引处的条目不是期望的类型,则抛出ConstantPoolException而不是ClassCastException。这允许类文件处理器指示常量池条目类型不匹配是类文件格式问题而不是处理器的问题。
四、类文件API示例
以下是一个使用类文件API生成简单Java方法的示例:
import java.lang.classfile.*;
public class ClassFileApiExample {
public static void main(String[] args) throws Exception {
// 创建一个ClassBuilder对象
ClassBuilder classBuilder = ClassFile.of()
.build(ClassDesc.of("com.example.HelloWorld"), clazz -> {
// 添加一个主方法
clazz.withMethod("main", MethodTypeDesc.of(CD_void, CD_arrayOf(CD_String)),
ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, methodBuilder -> {
// 使用CodeBuilder生成方法体
methodBuilder.withCode(codeBuilder -> {
// 获取System.out的引用
codeBuilder.getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream"));
// 加载字符串常量 "Hello, World!"
codeBuilder.ldc("Hello, World!");
// 调用PrintStream的println方法
codeBuilder.invokevirtual(ClassDesc.of("java.io.PrintStream"), "println", MethodTypeDesc.of(CD_void, CD_String));
// 返回
codeBuilder.return_();
});
});
});
// 将生成的类文件写入文件
byte[] classBytes = classBuilder.toByteArray();
Files.write(Path.of("./HelloWorld.class"), classBytes);
}
}
在这个示例中,我们创建了一个名为HelloWorld的类,并为其添加了一个主方法。主方法打印出字符串"Hello, World!"。通过类文件API,我们可以方便地生成和修改类文件,而无需依赖第三方库。
结构化并发
结构化并发概述
结构化并发是一种并发编程模型,它通过将不同线程中的相关任务视为单一工作单元来处理,从而简化了并发编程。结构化并发API的引入,旨在改进错误处理和取消操作,使开发者能够更容易地编写和管理并发代码。
结构化并发的关键组件
- StructuredTaskScope:这是结构化并发的核心类,用于管理并发任务的生命周期。开发者可以创建StructuredTaskScope实例,并在其作用域内启动多个并发任务。这些任务会在作用域结束时被自动等待(join),从而确保所有任务都已完成或已取消。
结构化并发的主要优势
- 简化并发编程:结构化并发通过提供清晰的并发任务管理模型,降低了并发编程的复杂度。
- 改进错误处理:在StructuredTaskScope作用域内启动的任务,如果任何一个任务抛出异常,该异常会被捕获并传播到作用域外,开发者可以统一处理这些异常。
- 提高代码可靠性:通过确保所有任务在作用域内被有效管理,结构化并发提高了代码的可靠性。
示例代码
以下是一个简单的示例,展示了如何使用StructuredTaskScope来管理并发任务:
import java.util.concurrent.*;
public class StructuredConcurrencyExample {
public static void main(String[] args) {
// 创建一个StructuredTaskScope实例
try (var scope = new StructuredTaskScope<Void>()) {
// 启动多个并发任务
scope.fork(() -> {
// 执行任务
System.out.println("Task 1 is running");
Thread.sleep(1000);
return null;
});
scope.fork(() -> {
// 执行任务
System.out.println("Task 2 is running");
Thread.sleep(500);
return null;
});
// 等待所有任务完成
scope.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,我们创建了一个StructuredTaskScope实例,并在其作用域内启动了两个并发任务。这两个任务会并行执行,并在作用域结束时被自动等待(join)。如果任何一个任务在执行过程中抛出异常,该异常会被捕获并传播到作用域外,最终由catch块捕获并处理。
结构化并发的应用场景
结构化并发适用于任何需要并发执行多个任务,并希望简化并发编程和管理并发任务生命周期的场景。例如,在处理Web请求、执行并行计算任务或处理I/O操作时,结构化并发都可以提供显著的帮助。
作用域值
作用域值概述
作用域值API的设计理念在于支持在线程内部和线程之间的共享不可变数据。与传统的线程局部变量相比,作用域值具有几个显著的优势:
- 数据不可变性:作用域值确保数据是不可变的,这减少了因随意修改线程局部变量导致的潜在不一致性问题。
- 访问范围限定:作用域值限定了数据的访问范围,只能在定义好的作用域中访问,增强了数据安全性。
- 简化数据流管理:与线程局部变量相比,作用域值简化了数据流的管理和推断过程。
- 降低空间和时间成本:特别是在与虚拟线程(JEP 444)和结构化并发(JEP 480)结合使用时,作用域值可以显著降低空间和时间成本。
作用域值API的主要变化
在JDK 23中,作用域值API的第三个预览版带来了关键增强。主要变化包括:
- 修改了ScopedValue.callWhere方法,使其操作参数成为一个函数式接口。这一改变允许Java编译器推断是否可能抛出检查异常,从而简化了代码结构。
- 删除了ScopedValue.getWhere方法,因为callWhere方法的新设计已经能够满足其需求。
作用域值示例
以下是一个简化的示例,展示了如何使用作用域值在Web框架中共享上下文数据,而不必显式地将上下文作为参数传递。
// 假设我们有一个Web框架,它需要在不同的方法之间共享上下文数据
// 传统的线程局部变量方法
// ThreadLocal<FrameworkContext> CONTEXT = new ThreadLocal<>();
// 使用作用域值API
public class Framework {
// 创建一个作用域值,用于共享不可变的上下文数据
private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.create();
// 处理请求的方法
public static void serve(HttpRequest request) {
FrameworkContext context = createContext(request);
// 设置上下文数据
CONTEXT.runWhere(context, () -> {
// 在这个作用域内,可以访问上下文数据
readKey(context.getKey());
});
}
// 读取键的方法
public static void readKey(String key) {
// 在这个作用域内,可以安全地访问CONTEXT中的上下文数据
FrameworkContext context = CONTEXT.get();
// 执行与键相关的操作
System.out.println("Key: " + key + ", Value: " + context.getValue(key));
}
// 创建上下文数据的方法
private static FrameworkContext createContext(HttpRequest request) {
// 根据请求创建上下文数据
// ...
return new FrameworkContext(/* 初始化上下文数据 */);
}
// 框架上下文类
public static class FrameworkContext {
private final Map<String, String> data;
public FrameworkContext(Map<String, String> data) {
this.data = data;
}
public String getKey() {
// 返回键
return data.keySet().iterator().next(); // 简化示例,实际中应根据逻辑返回键
}
public String getValue(String key) {
// 返回与键关联的值
return data.get(key);
}
}
}
// HttpRequest类是一个简化的表示,实际中应根据需求定义
public class HttpRequest {
// ...
}
在这个示例中,CONTEXT是一个作用域值,用于在线程内和子线程间共享不可变的FrameworkContext对象。在serve方法中,我们创建了一个FrameworkContext对象,并使用CONTEXT.runWhere方法将其设置在当前作用域内。然后,在readKey方法中,我们可以安全地访问CONTEXT中的上下文数据,而无需显式地将其作为参数传递。
Z垃圾收集器(ZGC)默认模式切换
背景与意义
ZGC是Java中的一种低延迟垃圾收集器,它旨在提供可预测的低停顿时间,同时支持大内存的应用。在JDK 23之前,ZGC主要运行在非分代模式下,这意味着它不会明确区分年轻代和老年代,而是对整个堆进行统一的垃圾收集。然而,这种模式在某些情况下可能不是最优的,因为它可能无法充分利用年轻代对象快速死亡的特点。
为了进一步优化ZGC的性能,JDK 23将ZGC的默认模式切换为分代模式。在分代模式下,ZGC会明确区分年轻代和老年代,并针对不同代的特点采用不同的垃圾收集策略。这有助于减少不必要的垃圾收集,提高收集效率,从而降低应用程序的停顿时间。
分代模式的工作原理
在分代模式下,ZGC会按照以下步骤进行垃圾收集:
- 年轻代收集:当年轻代中的对象数量达到阈值时,ZGC会触发年轻代收集。这个过程会快速回收不再被引用的对象,并将存活的对象晋升到老年代。由于年轻代中的对象通常生命周期较短,因此年轻代收集的频率会相对较高,但每次收集的时间通常较短。
- 老年代收集:当老年代中的对象数量达到阈值时,ZGC会触发老年代收集。这个过程会回收老年代中不再被引用的对象,并可能触发并发标记和并发清理等阶段。由于老年代中的对象通常生命周期较长,因此老年代收集的频率会相对较低,但每次收集的时间可能会较长。
通过明确区分年轻代和老年代,并采用不同的收集策略,ZGC的分代模式能够更有效地利用内存资源,提高垃圾收集的效率,从而降低应用程序的停顿时间。
示例
假设我们有一个使用JDK 23的Java应用程序,并且已经启用了ZGC作为垃圾收集器。在默认情况下,ZGC将运行在分代模式下。以下是一个简单的示例,展示了如何在Java应用程序中启用ZGC并观察其分代模式的行为:
public class ZGCExample {
public static void main(String[] args) {
// 创建一个大量对象的数组,以触发垃圾收集
Object[] largeArray = new Object[1000000];
// 填充数组,使部分对象快速变为垃圾
for (int i = 0; i < largeArray.length; i++) {
largeArray[i] = new byte[1024]; // 分配1KB的对象
// 假设某些对象在分配后立即变为垃圾
if (i % 100 == 0) {
largeArray[i] = null;
}
}
// 暂停一段时间,以便观察垃圾收集行为
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 应用程序继续运行...
}
}
在这个示例中,我们创建了一个包含大量对象的数组,并故意使部分对象在分配后立即变为垃圾。然后,我们暂停了一段时间,以便观察ZGC的分代模式如何回收这些垃圾对象。
当运行这个示例时,您可以通过添加JVM参数来启用ZGC并观察其日志输出。例如:
java -XX:+UseZGC -Xlog:gc* ZGCExample
在这个命令中,-XX:+UseZGC启用了ZGC作为垃圾收集器,-Xlog:gc*启用了GC日志记录,以便观察ZGC的行为。通过查看日志输出,您可以观察到ZGC如何进行年轻代收集和老年代收集,以及分代模式如何影响垃圾收集的性能。
其他重要更改
1. 弃用sun.misc.Unsafe中的内存访问方法(JEP 471):JDK 23计划弃用sun.misc.Unsafe中的内存访问方法,这是向更安全、更标准的API迈进的重要一步。这一变化鼓励开发者迁移到VarHandle API和外部函数及内存API。
2. 字符串模板的撤回:经过反馈和广泛讨论,JDK 23撤回了在JDK 22中预览的字符串模板特性,因为该特性在当前形式下不适合。
3. javac中默认禁用注解处理:从JDK 23开始,只有在显式配置注解处理或在javac命令行上显式请求运行注解处理时,才会运行注解处理。
展望
随着JDK 23的发布,Java社区将继续致力于提升Java的生产力和性能。Oracle透露了2024年Java的改进计划,涉及OpenJDK的多个项目,如Amber、Babylon和Valhalla。这些项目旨在通过开发小型、提高生产力的特性,扩展Java以支持如GPU等外部编程模型,以及通过值对象增强Java对象模型,消除长期存在的性能瓶颈。
总的来说,JDK 23的新特性和改进为Java开发者提供了更多的功能和灵活性,有助于提高代码的可读性、可维护性和性能。未来,随着Java的不断演进,我们可以期待更多创新和改进,使Java继续保持其在现代编程语言中的领先地位。
1479

被折叠的 条评论
为什么被折叠?



