首頁  >  文章  >  Java  >  Java 功能:詳細了解新 LTS 版本中最重要的變化

Java 功能:詳細了解新 LTS 版本中最重要的變化

WBOY
WBOY原創
2024-07-29 20:39:141002瀏覽

另一個 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。即:

  1. 字串範本
  2. 有序集合
  3. 開關和記錄模式的模式匹配
  4. 虛擬執行緒

事不宜遲,讓我們深入研究程式碼並探索這些更新。

字串模板(預覽)

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 集合層次結構。請查看下圖並將其與您在程式設計課程中可能學到的內容進行比較。您會注意到新增了三個新結構(以綠色突出顯示)。

Java features: A detailed look at the most important changes in the new LTS release
資料來源: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
}

Virtual Threads

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:

Java features: A detailed look at the most important changes in the new LTS release

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 程式語言的未來

上面描述的功能是我認為 Java 21 中最重要的功能。其中大多數功能並不像 JDK 17 中引入的一些功能那樣具有開創性,但它們仍然非常有用,並且很適合QOL(生活品質)發生變化。

但是,您也不應該忽視 JDK 21 的其他改進 - 我強烈鼓勵您分析完整列表並進一步探索所有功能。例如,我認為特別值得注意的一件事是 Vector API,它允許在某些受支援的 CPU 架構上進行向量運算——這在以前是不可能的。目前,它仍處於孵化器狀態/實驗階段(這就是為什麼我沒有在這裡更詳細地強調它),但它為 Java 的未來帶來了巨大的希望。

整體而言,Java 在各個領域的進步標誌著團隊對提高高需求應用程式的效率和效能的持續承諾。

如果您對 Java 有興趣,請務必查看我們的其他文章:

  1. Java 17 功能:版本 8 和 17 的比較。這些年來發生了哪些變化?
  2. JVM Kubernetes:為 Java 開發人員最佳化 Kubernetes
  3. Project Valhalla – Java 邁向更好的效能
  4. 進階 Java 面試問題:2023 年指南

Java 功能常見問題解答

以下是一些關於 JDK 21 以及 Java 本機介面和功能的常見問題的解答。

什麼是 Java SE?

Java SE(Java 平台,標準版)是在桌面和伺服器上開發和部署 Java 應用程式的基礎平台。

什麼是外部函數和記憶體 API?

這是一項預覽功能,可讓 Java 程式與 Java 執行時期以外的資料和程式碼進行互通。與 JNI 相比,API 使 Java 程式更安全地呼叫本機程式庫並處理本機資料。 API是安全存取外部記憶體和程式碼、有效率地呼叫外部函數的工具。

如何寫好Java程式碼?

其中一個關鍵方面是程式碼審查(您可以使用人工智慧程式碼審查工具來減少此過程的耗時)。

Java中的動態載入是什麼?

Java中的動態載入是指在執行時間載入類別或資源,而不是在程式初始啟動時載入。

什麼是結構化並發?

Java 中的結構化並發是一種以受控方式組織並發進程的方法,旨在增強多執行緒程式碼的可維護性、可靠性和可觀察性。

以上是Java 功能:詳細了解新 LTS 版本中最重要的變化的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn