daisuzz.log

JDK22でリリース予定のJEPを調べてみた

JDK 22 が3月にリリースされるので、どういった機能がリリース予定なのか調べてみた。

JDK 22 はリリース前(記事を書いている2024年1月5日時点でRampdown Phase One)のため、このエントリの内容から今後変わる可能性があります。

普段馴染みがないAPIについてのJEPも記載しているため、内容に不備があれば指摘してください。

リリース日

2024/03/19

リリース予定のJEP

JEP 423: Region Pinning for G1

G1GCにおいて、JNI(Java Native Interface)のcritical regionを使った操作中にG1GCを無効にしなくても良いようにすることで、Javaプログラムのレイテンシーを向上させる提案。

critical regionとは、Javaのガーベージコレクタがオブジェクトを移動させないように、特定のオブジェクトへのアクセスをロックする領域のこと。

JVMで管理されていないコード(e.g. C, C#)上で、JNIのJavaオブジェクトへのポインタを取得する関数を呼び出してから、その後取得したJavaオブジェクトを解放する関数を呼び出すまでの範囲というイメージ。

JNIを使ってJava外のコードからJavaのオブジェクトを直接ポインタを指定して参照しているため、GCによってJavaのオブジェクトの参照先が変更されると困る。そのためG1GCではデフォルトで、JNIのcritical regionではGCが無効化されている。

GCを無効化することでJavaプログラムのレイテンシーに影響があるため、このJEPでは、region pinningという手法を使っている。 region pinningの詳細は https://openjdk.org/jeps/423#Pinning-regions-during-minor-collection-operations 参照。

JEP 447: Statements before super(...) (Preview)

JDK 22 でPreview として追加されたJEP。

Javaのコンストラクタで呼び出すことができるsuper(...)というメソッドの前に文(statement)を書くことができるようになる。

JDK 21 までは、super(...)はコンストラクタの先頭で呼び出さないとコンパイルエラーになってしまう。 JDK 22 からは、super(...)の前に、引数の検証や事前準備などの処理が書けるようになり、より自然な形でロジックを記述できるようになる。

// JDK 21
public class Foo extends Bar {

    public Foo(long value) {
        // コンパイルエラー
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");

        super(value);             
    }

}

// JDK 22
public class Foo extends Bar {

    public Foo(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");

        super(value);             
    }

}

記述できる文には条件があるので、細かい条件は https://openjdk.org/jeps/447#Description 参照。

JEP 454: Foreign Function & Memory API

JavaプログラムからJVM管理外のコードを呼び出すためのAPIが提供される。 JDK 19, 20, 21でPreviewとして提案されてきたJEPがJDK 22で正式にリリースされた。

追加されたAPIjava.lang.foreign 配下に定義されているので、詳細はそちら参照。

JEP 456: Unnamed Variables & Patterns

利用しない変数やパターンに対して、_という名前を使えるようになる。JDK 21 でPreviewとして提案されていたJEP。 意図せず参照されることを防いだり、プログラムの意図をよりわかりやすくすることが目的。

// 無名変数のコード例
static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order _ : orders)    // Unnamed variable
        total++;
    return total;
}

// 無名パターンのコード例
switch (ball) {
    case RedBall _   -> process(ball);          // Unnamed pattern variable
    case BlueBall _  -> process(ball);          // Unnamed pattern variable
    case GreenBall _ -> stopProcessing();       // Unnamed pattern variable
}

_が使える変数には条件があるので、詳細は https://openjdk.org/jeps/456#Unnamed-variables を参照。

JEP 457: Class-File API (Preview)

Javaのクラスファイルの解析や生成や変換を行うための標準APIを提供。 JDK 22 でPreviewとして追加されたJEP。

クラスファイルを扱うライブラリとしてASMなどのサードパーティライブラリがあり、OpenJDKでもASMを利用している。 Javaのリリースサイクルが6ヶ月ごとになりクラスファイルの形式も頻繁に変わるが、クラスファイルライブラリはリリース済みのJavaのバージョンまでしか対応できないため、サードパーティライブラリの場合、最新のJavaのクラスファイルのサポートができない可能性がある。

こうした現状を解決するため、標準ライブラリとしてクラスファイルを扱うAPIを用意しようというのがこのJEPの目的。

java.lang.classfile (Java SE 22 [ad-hoc build]) 配下にAPIが用意されている。

JEP 458: Launch Multi-File Source-Code Programs

複数のJavaファイルで構成されるプログラムをjavaコマンドを使って実行できるようになる。

Prog.javaファイルとHelper.javaを用意して、JDK 21 と JDK 22 それぞれでjavaコマンドで実行してみる。

class Prog {
    public static void main(String[] args) {
        Helper.run();
    }
}
class Helper {
    static void run() {
        System.out.println("Hello!");
    }
}

JDK 21 だとコンパイルエラーになる。

$ java --version
openjdk 21.0.1 2023-10-17 LTS
OpenJDK Runtime Environment Corretto-21.0.1.12.1 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.1.12.1 (build 21.0.1+12-LTS, mixed mode, sharing)

$ java Prog.java
Prog.java:3: エラー: シンボルを見つけられません
    Helper.run();
    ^
  シンボル:   変数 Helper
  場所: クラス Prog
エラー1個
エラー: コンパイルが失敗しました

JDK 22 だと正常に動作する。

$ java --version
openjdk 22-ea 2024-03-19
OpenJDK Runtime Environment (build 22-ea+29-2286)
OpenJDK 64-Bit Server VM (build 22-ea+29-2286, mixed mode, sharing)

$ java Prog.java
Hello!

内部的には、java Prog.java コマンドを実行すると、Prog クラスがメモリ内でコンパイルされ、その main メソッドが呼び出される。 Prog クラスのコードが Helper クラスを参照しているため、アプリケーションランチャー(javaコマンド)はファイルシステム内の Helper.java ファイルを見つけ、そのクラスをメモリ内でコンパイルする。 もしHelper クラスが他のクラスを参照している場合、ランチャーはそのクラスも同様に見つけてコンパイルする、といった流れ。

このJEPによって、プロジェクトの開始時やプロトタイプ作成時にMavenやGradleなどのビルドツールを導入せず、Javaのコードを書き始めることができる。

JEP 459: String Templates (Second Preview)

JDK 21 でPreviewとして提案されていたJEPが、JDK22 でSecond Previewとして提案。

他のプログラミング言語でも採用されている文字列テンプレートをJavaに導入するJEP。

STRというテンプレートプロセッサを使って文字列を埋め込むことができる。

String name = "Joan";
String info = STR."My name is \{name}";
assert info.equals("My name is Joan"); // true

複数行にも対応している。

String title = "My Web Page";
String text = "Hello, world";
String html = STR.""" 
 <html>
 <head>
 <title>\{title}</title>
 </head>
 <body>
 <p>\{text}</p>
 </body>
 </html>
 """;

JEP 460: Vector API (Seventh Incubator)

CPUアーキテクチャ上で最適なベクトル命令を表現するAPIを導入する。

JDK 16からIncubatorとして提案されているJEP。

Six Incubatorまではバイト配列のみをサポートしていたが、今回のJEPで任意のプリミティブ型の配列をサポートするようになった。

JEP 461: Stream Gatherers (Preview)

JDK 22 でPreview として追加されたJEP。

Stream APIを拡張してカスタム中間操作をサポートするAPIとしてStream::gather(Gatherer)を提供。

これによって、既存の標準APIとして提供されている中間操作では簡単に実現できないデータ変換を定義することが可能になる。

以下コード例。

import java.util.stream.Stream;
import java.util.stream.Gatherers;

public class StreamGatherExample {
    public static void main(String[] args) {
        // Streamの作成
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);

        // 標準APIとして用意されているGatherers::windowFixed を使用して、要素を3つのグループに分ける
        var result = stream.gather(Gatherers.windowFixed(3)).toList();

        // 結果の表示
        result.forEach(System.out::println);
    }
}

Gathererというインターフェースが用意されており、このインターフェースを実装することで独自の中間操作を定義できる。

またGathererインターフェースを実装した標準APIGatherersというクラスに定義されている。

JEP 462: Structured Concurrency (Second Preview)

Javaの並行プログラミングを簡素化するために、構造化された並行処理を扱うAPIを導入。

JDK 19 でIncubatorとして提案され、JDK 21でPreviewとして提案されていたJEP。フィードバックを募集するためJDK 21から内容は変わらずそのままJDK 22 でSecond Previewとして提案されている。

従来のExecutorServiceを使った並行プログラミングでは、あるサブタスクが失敗した場合、他のサブタスクは継続して実行されてしまい、リソースの無駄遣いや他のタスクに干渉する可能性があったり、サブタスク間の関係がコード上では明確でないため、トラブルシューティングが難しいという問題がある。

このJEPでは、StructuredTaskScopeというクラスで、タスクとサブタスクを一つの単位として表現できるようにしている。 これによって、サブタスクを個別に操作したり、サブタスクの成功した結果や例外を親タスクに集約して処理することができるようになる。

以下コード例。

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

        // サブタスク1
        Supplier<String>  user  = scope.fork(() -> findUser());
  
        // サブタスク2
        Supplier<Integer> order = scope.fork(() -> fetchOrder());

        // サブタスクを結合して、いずれかの処理が失敗した場合、例外を呼び出しもとに伝搬させる
        scope.join()            
             .throwIfFailed();  

        // 両方のサブタスクが成功した場合、レスポンスを返す
        return new Response(user.get(), order.get());
    }
}

JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)

JDK 21 でPreviewとして提案されていたJEPが、JDK 22 でSecond Previewとして提案。

Java初心者がJavaを触るときにおまじないとなる部分を減らすためや、簡単なJavaスクリプトを書きやすくするためのJEP。

ソースファイルの中でクラス宣言がない場合、暗黙的にクラスを宣言したとみなして、システムによって選択された名前のクラスとして扱われるようになった。 また、mainメソッドの選択プロセスが簡素化され、String[]パラメータを持つmainメソッドがあれば、そのメソッドを呼び出し、そうでなければ、パラメータを持たないmainメソッドが呼び出されるようになった。

変更前

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

変更後

void main(){
    System.out.println("Hello World");
}

JEP 464: Scoped Values (Second Preview)

JDK 20 で Incubatorとして提案され、JDK 21 で Previewとして提案されていたJEP。 フィードバックを集める目的のため、JDK 21 から内容を変更せずJDK 22 では Second Previewとして提案されている。

不変なデータをスレッド内やスレッド間で共有するための新しい ScopedValue クラスを追加している。

元々Javaには ThreadLocal クラスが用意されているが、ThreadLocal は安全性や効率性に課題がある。 安全性という観点では、ThreadLocal変数は可変だったり、ThreadLocalにセットした変数の生存期間がThreadの生存期間と同じになることが多く意図せず参照できてしまう設計になっている。 また、効率性という観点では、親のThreadLocalを子スレッドが引き継ぐ設計になっているため JDK 21 で追加された VirtualThread で大量のスレッドを作るケースではThreadLocalはオーバヘッドが多くなってしまう。

こうした課題を解消するために ScopedValueが提案されているという背景。

ScopedValueを使ったコード例は以下。

final static ScopedValue<String> SCOPED_VALUE = ScopedValue.newInstance();

ScopedValue.where(SCOPED_VALUE, "hello")
                       .run(() -> System.out.println(SCOPED_VALUE.get()));

まず、あるクラスの final static フィールドとしてScopedValue クラスのインスタンスを定義する。

whereメソッドでScopedValueに値をバインドして、runメソッドでScopedValueを利用する処理をラムダ式で渡す。

ラムダ式の中で呼ばれる処理では、getメソッドを使ってScopedValueに格納された値を取得することができる。 例ではラムダ式の中でgetメソッドを呼び出しているが、ラムダ式の中で間接的に呼ばれる処理の中でもgetメソッドを使ってScopedValueの値を取得することができる。

参考情報