Java SE 24 新增特性
作者:Grey
原文地址:
源码
Patterns、instanceof 和 switch 可以匹配更多类型(第二次预览)
通过允许在所有模式上下文中使用原始类型来增强模式匹配,Java 16 中引入了与 instanceof 的模式匹配,在 Java 21 中引入了与 switch 的模式匹配。
public class PrimitiveTypesTest { void main() { test1("hello world"); test2("hello world"); test1(56); test2(56); test1(java.time.LocalDate.now()); test2(java.time.LocalDate.now()); } private static void test1(Object obj) { if (obj instanceof String s && s.length() >= 5) { System.out.println(s.toUpperCase()); } else if (obj instanceof Integer i) { System.out.println(i * i); } else { System.out.println(obj); } } private static void test2(Object obj) { switch (obj) { case String s when s.length() >= 5 -> System.out.println(s.toUpperCase()); case Integer i -> System.out.println(i * i); case null, default -> System.out.println(obj); } } }
JEP 455 在 Java 23 中引入了两项变更:
-
可以在 switch 表达式和语句中使用所有基元类型,包括 long、float、double 和 boolean。
-
其次,我们还可以在模式匹配中使用所有基元类型,包括 instanceof 和 switch。
在这两种情况下,即通过 long、float、double 和布尔类型进行 switch 以及使用基元变量进行模式匹配时,与所有新的 switch 功能一样,switch 必须要涵盖所有可能的情况。
private static void test3(int x) { switch (x) { case 1, 2, 3 -> System.out.println("Low"); case 4, 5, 6 -> System.out.println("Medium"); case 7, 8, 9 -> System.out.println("High"); } }
模块导入声明(第二次预览)
这个功能在JDK 23 上是第一次预览,主要功能是通过简洁地导入模块导出的所有包的功能来增简化了模块库的重复使用,但不要求导入代码本身必须在模块中。
自 Java 1.0 起,java.lang
包中的所有类都会自动导入到每个 .java 文件中。这就是为什么我们无需导入语句就能使用 Object
、String
、Integer
、Exception
、Thread
等类的原因。
我们还可以导入完整的包。例如,导入 java.util.*
意味着我们不必单独导入 List
、Set
、Map
、ArrayList
、HashSet
和 HashMap
等类。
JEP 467现在允许我们导入完整的模块,更准确地说,是导入模块导出的包中的所有类。
例如,我们可以按如下方式导入完整的 java.base
模块,然后使用该模块中的类(例如 List
、Map
、Collectors
、Stream
),而无需进一步导入:
package git.snippets.jdk23; import module java.base; public class ModuleImportDeclarationsTest { void main() { System.out.println(groupByFirstLetter("a", "abc", "bcd", "ddd", "dddc", "dfc", "bc")); } public static Map<Character, List<String>> groupByFirstLetter(String... values) { return Stream.of(values).collect(Collectors.groupingBy(s -> Character.toUpperCase(s.charAt(0)))); } }
如果有两个同名的导入类,例如下面示例中的 Date,编译器就会出错:
import module java.base; import module java.sql;
如果一个导入模块临时导入了另一个模块,那么我们也可以使用临时导入模块导出包中的所有类,而无需显式导入。
例如,java.sql 模块转义导入了 java.xml 模块:
module java.sql { . . . requires transitive java.xml; . . . }
因此,在下面的示例中,我们不需要显式导入 SAXParserFactory 和 SAXParser,也不需要显式导入 java.xml 模块:
SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser saxParser = factory.newSAXParser();
灵活的构造函数主体(第三次预览)
这个功能在 JDK 23 上是第二次预览,现在是第三次预览,下述代码中,Child1的构造函数,只能先通过super构造父类,然后才能初始化子类的 b 这个变量。
public class FlexibleConstructorBodies { void main() { new Child1(1, 2); } } class Parent { private final int a; public Parent(int a) { this.a = a; printMe(); } void printMe() { System.out.println("a = " + a); } } // JDK 23 之前 class Child1 extends Parent { private final int b; public Child1(int a, int b) { super(verifyParamsAndReturnA(a, b)); this.b = b; } @Override void printMe() { super.printMe(); System.out.println("b = " + b); } private static int verifyParamsAndReturnA(int a, int b) { if (a < 0 || b < 0) throw new IllegalArgumentException(); return a; } }
当我们执行
new Child1(1,2);
这段代码的时候,本来我们期待返回的是
a = 1 b = 2
但是由于父类在构造时候调用了printMe()
,且这个调用是在 b 变量初始化之前调用的,所以导致程序执行的结果是
a = 1 b = 0
今后,在使用 super(...) 调用父类构造函数之前,以及在使用 this(...) 调用本类的构造函数之前,我们可以执行任何不访问当前构造实例(即不访问其字段)的代码,
此外,我们还可以初始化正在构造的实例的字段。详见JEP 482
在 JDK 24 上,上述代码可以调整为:
class Child2 extends Parent { private final int b; public Child2(int a, int b) { if (a < 0 || b < 0) throw new IllegalArgumentException(); this.b = b; super(a); } @Override void printMe() { super.printMe(); System.out.println("b = " + b); } }
其中构造函数中,a和b的初始化和判断,都可以在super(...)函数调用之前,
执行
new Child2(1,2);
打印结果为预期结果
a = 1 b = 2
main方法的精简写法(第四次预览)
最早出现在 JDK 21 中,见Java SE 21 新增特性
原来我们写一个main方法,需要
public class UnnamedClassesAndInstanceMainMethodsTest { public static void main(String[] args) { System.out.println("Hello World!"); } }
而且Java文件的名称需要和UnnamedClassesAndInstanceMainMethodsTest保持一致,到了JDK 24,上述代码可以简化成
void main() { System.out.println("hello world"); }
甚至连 public class ... 这段也不需要写,在JDK 24版本中,这个功能是第四次预览。
结构化并发(第四次预览)
JEP 499 引入了结构化并发(Structured Concurrency),它确保相关的任务一起启动、一起管理,使并发编程更安全、更易于理解。
让我们通过一个简单的示例来理解它:并行获取用户数据和订单数据,并比较传统方式和结构化并发的实现方式。
传统方式(没有结构化并发)
在传统方法中,我们使用 ExecutorService
并手动管理任务执行和取消:
import java.util.concurrent.*; public class TraditionalConcurrencyExample { private static final ExecutorService executor = Executors.newFixedThreadPool(2); public static void main(String[] args) throws ExecutionException, InterruptedException { Future<String> userFuture = executor.submit(() -> fetchUserData()); Future<String> orderFuture = executor.submit(() -> fetchOrderData()); try { String userData = userFuture.get(); // 阻塞直到用户数据返回 String orderData = orderFuture.get(); // 阻塞直到订单数据返回 System.out.println("用户: " + userData + ", 订单: " + orderData); } finally { executor.shutdown(); } } static String fetchUserData() { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "用户数据"; } static String fetchOrderData() { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "订单数据"; } }
存在的问题:
子任务管理复杂——没有清晰的父子关系,难以控制任务的生命周期。
手动异常处理——需要自行管理任务失败时的行为。
资源泄漏风险——如果一个任务失败,另一个任务可能仍在运行,可能导致不一致的状态。
使用结构化并发
现在,我们使用 StructuredTaskScope
让任务在相同的作用域中执行,并确保它们要么一起完成,要么一起失败:
import java.util.concurrent.*; import jdk.incubator.concurrent.StructuredTaskScope; public class StructuredConcurrencyExample { public static void main(String[] args) throws InterruptedException, ExecutionException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { StructuredTaskScope.Subtask<String> userTask = scope.fork(() -> fetchUserData()); StructuredTaskScope.Subtask<String> orderTask = scope.fork(() -> fetchOrderData()); scope.join(); // 等待所有任务完成 scope.throwIfFailed(); // 如果有任务失败,取消所有任务并抛出异常 String userData = userTask.get(); String orderData = orderTask.get(); System.out.println("用户: " + userData + ", 订单: " + orderData); } } static String fetchUserData() { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "用户数据"; } static String fetchOrderData() { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "订单数据"; } }
结构化并发的优势如下:
任务管理更清晰——所有任务都在 StructuredTaskScope
作用域内,代码更加易读。
自动清理——如果任何一个任务失败,所有任务都会被取消,不会出现部分完成的情况。
更好的错误处理——异常可以在 scope.throwIfFailed()
统一管理,避免手动 try-catch。
更容易调试——所有任务都有明确的父作用域,方便排查问题。
结构化并发(Structured Concurrency)让并行任务的执行更安全、更易维护。它确保任务要么一起成功完成,要么一起失败取消,避免了传统并发编程中的各种问题,如任务泄漏、异常传播困难等。