ホームページ  >  記事  >  Java  >  Java 機能: 新しい LTS リリースの最も重要な変更点の詳細

Java 機能: 新しい LTS リリースの最も重要な変更点の詳細

WBOY
WBOYオリジナル
2024-07-29 20:39:141019ブラウズ

別の LTS Java リリースがすでにリリースされており、いくつかのエキサイティングな変更と改善がもたらされています。最も重要な Java 21 機能を分析し、それらが実際にどのように機能するかを確認し、このテクノロジーの将来におけるそれらの重要性を予測してみましょう。

Java プラットフォームが 6 か月のリリース サイクルを採用して以来、私たちは「Java は今年廃止されるのか?」などの長年の疑問を克服しました。または「新しいバージョンに移行する価値はありますか?」最初のリリースから 28 年が経過したにもかかわらず、Java は成長を続けており、多くの新しいプロジェクトの主要なプログラミング言語として人気があり続けています。

Java 17 は重要なマイルストーンでしたが、Java 21 が 17 に代わって次の長期サポート リリース (LTS) になりました。 Java 開発者にとって、このバージョンによってもたらされる変更点や新機能について常に最新の情報を得ることが重要です。 Java 17 の機能について記事で詳しく説明した同僚の Darek に触発されて、私も同様の方法で JDK 21 について議論することにしました。

JDK 21 は、合計 15 の JEP (JDK Enhancement Proposal) で構成されています。公式 Java サイトで完全なリストを確認できます。この記事では、特に注目に値すると思われる Java 21 JEP をいくつか取り上げます。つまり:

  1. 文字列テンプレート
  2. シーケンスされたコレクション
  3. スイッチとレコードパターンのパターンマッチング
  4. 仮想スレッド

早速、コードを詳しく調べてこれらの更新内容を確認してみましょう。

文字列テンプレート (プレビュー)

Spring テンプレート機能はまだプレビュー モードです。これを使用するには、コンパイラ引数に –enable-preview フラグを追加する必要があります。ただし、プレビュー段階にもかかわらず、言及することにしました。なぜ?なぜなら、多くの引数を含むログ メッセージや SQL ステートメントを作成したり、どのプレースホルダーが指定された引数に置き換えられるかを解読したりする必要があるたびに、非常にイライラするからです。 Spring Templates は、私 (そしてあなた) のそれを助けることを約束します。

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 コレクション階層が導入されました。下の図を見て、おそらくプログラミングの授業で学んだことと比較してください。 3 つの新しい構造が追加されていることがわかります (緑色で強調表示されています)。

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();

小さな変更ですが、私の意見では、とても便利で使える機能です。

パターンマッチングとパターンの記録

スイッチのパターンマッチングとレコードパターンは似ているので、一緒に説明します。レコード パターンは新しい機能です。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);
}

醜いインスタンスを取り除くことができたらどうなるでしょうか? 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 の改善点も軽視すべきではありません。完全なリストを分析し、すべての機能をさらに詳しく調査することを強くお勧めします。たとえば、私が特に注目すべきと考えているものの 1 つは 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 Platform, Standard Edition) は、デスクトップおよびサーバー上で Java アプリケーションを開発およびデプロイするための基本的なプラットフォームです。

外部関数およびメモリ API とは何ですか?

これは、Java プログラムが Java ランタイム外のデータやコードと相互運用できるようにするプレビュー機能です。この API を使用すると、Java プログラムはネイティブ ライブラリを呼び出し、JNI の場合よりも安全にネイティブ データを処理できるようになります。 API は、外部メモリやコードに安全にアクセスし、外部関数を効率的に呼び出すためのツールです。

Java コードをうまく書くにはどうすればよいでしょうか?

重要な側面の 1 つはコード レビューです (AI コード レビュー ツールを使用すると、このプロセスの時間を少し短縮できます)。

Javaの動的ロードとは何ですか?

Java における動的ロードとは、プログラムの初期起動時ではなく、実行時にクラスまたはリソースをロードすることを指します。

構造化された同時実行とは何ですか?

Java の構造化同時実行性は、マルチスレッド コードの保守性、信頼性、可観測性を強化することを目的として、制御された方法で同時プロセスを編成するアプローチです。

以上がJava 機能: 新しい LTS リリースの最も重要な変更点の詳細の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。