另一个 LTS Java 版本已经发布,带来了一些令人兴奋的变化和改进。让我们分析最重要的 Java 21 功能,看看它们在实践中如何工作,并尝试预测它们对这项技术的未来的重要性。
自从 Java 平台采用六个月的发布周期以来,我们已经摆脱了诸如“Java 今年会消亡吗?”等长期存在的问题。或“是否值得迁移到新版本?”。尽管自首次发布以来已有 28 年,Java 仍在蓬勃发展,并且仍然是许多新项目的主要编程语言的流行选择。
Java 17 是一个重要的里程碑,但 Java 21 现已取代 17 成为下一个长期支持版本 (LTS)。 Java 开发人员必须随时了解此版本带来的更改和新功能。受到我的同事 Darek(他在文章中详细介绍了 Java 17 功能)的启发,我决定以类似的方式讨论 JDK 21。
JDK 21 总共包含 15 个 JEP(JDK 增强提案)。您可以在 Java 官方网站上查看完整列表。在本文中,我将重点介绍几个我认为特别值得注意的 Java 21 JEP。即:
事不宜迟,让我们深入研究代码并探索这些更新。
Spring 模板功能仍处于预览模式。要使用它,您必须将 –enable-preview 标志添加到编译器参数中。然而,尽管它处于预览状态,我还是决定提及它。为什么?因为每次我必须编写包含许多参数的日志消息或 sql 语句或破译哪个占位符将被给定的参数替换时,我都会感到非常恼火。 Spring 模板承诺会帮助我(和你)。
正如 JEP 文档所述,Spring 模板的目的是“通过轻松表达包含运行时计算值的字符串来简化 Java 程序的编写”。
让我们看看它是否真的更简单。
“旧方法”是在 String 对象上使用 formatted() 方法:
var msg = "Log message param1: %s, pram2: %s".formatted(p1, p2);
现在,使用 StringTemplate.Processor (STR),它看起来像这样:
var interpolated = STR."Log message param1: \{p1}, param2: \{p2}";
对于像上面这样的短文本,利润可能不是那么明显 – 但相信我,当涉及到大文本块(json、sql 语句)时,命名参数会对你有很大帮助。
Java 21 引入了新的 Java 集合层次结构。查看下图并将其与您在编程课程中可能学到的内容进行比较。您会注意到添加了三个新结构(以绿色突出显示)。
来源:JEP 431
有序集合引入了新的内置 Java API,增强了对有序数据集的操作。该 API 不仅可以方便地访问集合的第一个和最后一个元素,还可以实现高效的遍历、在特定位置插入以及检索子序列。这些增强功能使依赖于元素顺序的操作变得更简单、更直观,在处理列表和类似数据结构时提高了性能和代码可读性。
这是 SequencedCollection 接口的完整列表:
public interface SequencedCollection<E> extends Collection<E> { SequencedCollection<E> reversed(); default void addFirst(E e) { throw new UnsupportedOperationException(); } default void addLast(E e) { throw new UnsupportedOperationException(); } default E getFirst() { return this.iterator().next(); } default E getLast() { return this.reversed().iterator().next(); } default E removeFirst() { var it = this.iterator(); E e = it.next(); it.remove(); return e; } default E removeLast() { var it = this.reversed().iterator(); E e = it.next(); it.remove(); return e; } }
所以,现在,而不是:
var first = myList.stream().findFirst().get(); var anotherFirst = myList.get(0); var last = myList.get(myList.size() - 1);
我们可以写:
var first = sequencedCollection.getFirst(); var last = sequencedCollection.getLast(); var reversed = sequencedCollection.reversed();
一个小变化,但恕我直言,这是一个非常方便且可用的功能。
由于switch的模式匹配和记录模式的相似性,我将它们放在一起描述。记录模式是一个新功能 - 它们已在 Java 19 中引入(作为预览版)。另一方面,switch 的模式匹配有点像扩展的 instanceof 表达式的延续。它为 switch 语句引入了新的可能语法,使您可以更轻松地表达复杂的面向数据的查询。
为了这个示例,让我们忘记 OOP 的基础知识并手动解构雇员对象(employee 是一个 POJO 类)。
Java 21 之前,它看起来像这样:
if (employee instanceof Manager e) { System.out.printf("I’m dealing with manager of %s department%n", e.department); } else if (employee instanceof Engineer e) { System.out.printf("I’m dealing with %s engineer.%n", e.speciality); } else { throw new IllegalStateException("Unexpected value: " + employee); }
如果我们可以摆脱丑陋的instanceof怎么办?好吧,现在我们可以了,这要归功于 Java 21 模式匹配的强大功能:
switch (employee) { case Manager m -> printf("Manager of %s department%n", m.department); case Engineer e -> printf("I%s engineer.%n", e.speciality); default -> throw new IllegalStateException("Unexpected value: " + employee); }
While talking about the switch statement, we can also discuss the Record Patterns feature. When dealing with a Java Record, it allows us to do much more than with a standard Java class:
switch (shape) { // shape is a record case Rectangle(int a, int b) -> System.out.printf("Area of rectangle [%d, %d] is: %d.%n", a, b, shape.calculateArea()); case Square(int a) -> System.out.printf("Area of square [%d] is: %d.%n", a, shape.calculateArea()); default -> throw new IllegalStateException("Unexpected value: " + shape); }
As the code shows, with that syntax, record fields are easily accessible. Moreover, we can put some additional logic to our case statements:
switch (shape) { case Rectangle(int a, int b) when a < 0 || b < 0 -> System.out.printf("Incorrect values for rectangle [%d, %d].%n", a, b); case Square(int a) when a < 0 -> System.out.printf("Incorrect values for square [%d].%n", a); default -> System.out.println("Created shape is correct.%n"); }
We can use similar syntax for the if statements. Also, in the example below, we can see that Record Patterns also work for nested records:
if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) { //sth }
The Virtual Threads feature is probably the hottest one among all Java 21 – or at least one the Java developers have waited the most for. As JEP documentation (linked in the previous sentence) says, one of the goals of the virtual threads was to “enable server applications written in the simple thread-per-request style to scale with near-optimal hardware utilization”. However, does this mean we should migrate our entire code that uses java.lang.Thread?
First, let’s examine the problem with the approach that existed before Java 21 (in fact, pretty much since Java’s first release). We can approximate that one java.lang.Thread consumes (depending on OS and configuration) about 2 to 8 MB of memory. However, the important thing here is that one Java Thread is mapped 1:1 to a kernel thread. For simple web apps which use a “one thread per request” approach, we can easily calculate that either our machine will be “killed” when traffic increases (it won’t be able to handle the load) or we’ll be forced to purchase a device with more RAM, and our AWS bills will increase as a result.
Of course, virtual threads are not the only way to handle this problem. We have asynchronous programming (frameworks like WebFlux or native Java API like CompletableFuture). However, for some reason – maybe because of the “unfriendly API” or high entry threshold – these solutions aren’t that popular.
Virtual Threads aren’t overseen or scheduled by the operating system. Rather, their scheduling is handled by the JVM. While real tasks must be executed in a platform thread, the JVM employs so-called carrier threads — essentially platform threads — to “carry” any virtual thread when it is due for execution. Virtual Threads are designed to be lightweight and use much less memory than standard platform threads.
The diagram below shows how Virtual Threads are connected to platform and OS threads:
So, to see how Virtual Threads are used by Platform Threads, let’s run code that starts (1 + number of CPUs the machine has, in my case 8 cores) virtual threads.
var numberOfCores = 8; // final ThreadFactory factory = Thread.ofVirtual().name("vt-", 0).factory(); try (var executor = Executors.newThreadPerTaskExecutor(factory)) { IntStream.range(0, numberOfCores + 1) .forEach(i -> executor.submit(() -> { var thread = Thread.currentThread(); System.out.println(STR."[\{thread}] VT number: \{i}"); try { sleep(Duration.ofSeconds(1L)); } catch (InterruptedException e) { throw new RuntimeException(e); } })); }
Output looks like this:
[VirtualThread[#29,vt-6]/runnable@ForkJoinPool-1-worker-7] VT number: 6 [VirtualThread[#26,vt-4]/runnable@ForkJoinPool-1-worker-5] VT number: 4 [VirtualThread[#30,vt-7]/runnable@ForkJoinPool-1-worker-8] VT number: 7 [VirtualThread[#24,vt-2]/runnable@ForkJoinPool-1-worker-3] VT number: 2 [VirtualThread[#23,vt-1]/runnable@ForkJoinPool-1-worker-2] VT number: 1 [VirtualThread[#27,vt-5]/runnable@ForkJoinPool-1-worker-6] VT number: 5 [VirtualThread[#31,vt-8]/runnable@ForkJoinPool-1-worker-6] VT number: 8 [VirtualThread[#25,vt-3]/runnable@ForkJoinPool-1-worker-4] VT number: 3 [VirtualThread[#21,vt-0]/runnable@ForkJoinPool-1-worker-1] VT number: 0
So, ForkJonPool-1-worker-X Platform Threads are our carrier threads that manage our virtual threads. We observe that Virtual Threads number 5 and 8 are using the same carrier thread number 6.
The last thing about Virtual Threads I want to show you is how they can help you with the blocking I/O operations.
Whenever a Virtual Thread encounters a blocking operation, such as I/O tasks, the JVM efficiently detaches it from the underlying physical thread (the carrier thread). This detachment is critical because it frees up the carrier thread to run other Virtual Threads instead of being idle, waiting for the blocking operation to complete. As a result, a single carrier thread can multiplex many Virtual Threads, which could number in the thousands or even millions, depending on the available memory and the nature of tasks performed.
Let’s try to simulate this behavior. To do this, we will force our code to use only one CPU core, with only 2 virtual threads – for better clarity.
System.setProperty("jdk.virtualThreadScheduler.parallelism", "1"); System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1"); System.setProperty("jdk.virtualThreadScheduler.minRunnable", "1");
Thread 1:
Thread v1 = Thread.ofVirtual().name("long-running-thread").start( () -> { var thread = Thread.currentThread(); while (true) { try { Thread.sleep(250L); System.out.println(STR."[\{thread}] - Handling http request ...."); } catch (InterruptedException e) { throw new RuntimeException(e); } } } );
Thread 2:
Thread v2 = Thread.ofVirtual().name("entertainment-thread").start( () -> { try { Thread.sleep(1000L); } catch (InterruptedException e) { throw new RuntimeException(e); } var thread = Thread.currentThread(); System.out.println(STR."[\{thread}] - Executing when 'http-thread' hit 'sleep' function"); } );
Execution:
v1.join(); v2.join();
Result:
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#23,entertainment-thread]/runnable@ForkJoinPool-1-worker-1] - Executing when 'http-thread' hit 'sleep' function [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
We observe that both Virtual Threads (long-running-thread and entertainment-thread) are being carried by only one Platform Thread which is ForkJoinPool-1-worker-1.
To summarize, this model enables Java applications to achieve high levels of concurrency and scalability with much lower overhead than traditional thread models, where each thread maps directly to a single operating system thread. It’s worth noting that virtual threads are a vast topic, and what I’ve described is only a small fraction. I strongly encourage you to learn more about the scheduling, pinned threads and the internals of VirtualThreads.
上面描述的功能是我认为 Java 21 中最重要的功能。其中大多数功能并不像 JDK 17 中引入的一些功能那样具有开创性,但它们仍然非常有用,并且很适合QOL(生活质量)发生变化。
但是,您也不应该忽视 JDK 21 的其他改进 - 我强烈鼓励您分析完整列表并进一步探索所有功能。例如,我认为特别值得注意的一件事是 Vector API,它允许在某些受支持的 CPU 架构上进行矢量计算——这在以前是不可能的。目前,它仍处于孵化器状态/实验阶段(这就是为什么我没有在这里更详细地强调它),但它为 Java 的未来带来了巨大的希望。
总体而言,Java 在各个领域取得的进步标志着团队对提高高需求应用程序的效率和性能的持续承诺。
如果您对 Java 感兴趣,请务必查看我们的其他一些文章:
以下是有关 JDK 21 以及 Java 本机接口和功能的一些常见问题的解答。
Java SE(Java 平台,标准版)是在桌面和服务器上开发和部署 Java 应用程序的基础平台。
这是一项预览功能,可让 Java 程序与 Java 运行时之外的数据和代码进行互操作。与 JNI 相比,API 使 Java 程序能够更安全地调用本机库并处理本机数据。 API是安全访问外部内存和代码、高效调用外部函数的工具。
其中一个关键方面是代码审查(您可以使用人工智能代码审查工具来减少此过程的耗时)。
Java中的动态加载是指在运行时加载类或资源,而不是在程序初始启动时加载。
Java 中的结构化并发是一种以受控方式组织并发进程的方法,旨在增强多线程代码的可维护性、可靠性和可观察性。
以上是Java 功能:详细了解新 LTS 版本中最重要的变化的详细内容。更多信息请关注PHP中文网其他相关文章!