🍅

Java23の新機能を学ぶ

2024/10/01に公開

Java23でJavaDocがMarkdownで書けるという噂を耳にし、これを機に新機能を学んでみる。
(Java23と銘打ってはいるが、実際にはもっと前のバージョンでプレビュー機能になったものも多い。)

Java23について

https://www.oracle.com/jp/news/announcement/oracle-releases-java-23-2024-09-17/
2024/9/17にリリースされ、すでにAmazon Correttoなどのディストリビューションで利用可能のようだ。
プレスリリース内に登場する単語を調べてみる。

The goal of Project Amber is to explore and incubate smaller, productivity-oriented Java language features that have been accepted as candidate JEPs in the OpenJDK JEP Process.

とあり、javaの生産性向上を目的とした小規模な改善のJEP(後述)を提案するプロジェクトとのこと。

  • JEP(JDK Enhancement Proposal)
    JDKに対する拡張の提案のことで、Oracleが中心になってやっている活動。主に小規模で実装にフォーカスしたものが多い。
    大規模かつ公式な標準化プロセスは別にJSR(Java Specification Request)がある。
    Previewフェーズがあり、各JEPの状況はJEP 0:JEP Indexとして管理されているようだ。
    https://openjdk.org/jeps/0

This JEP is the index of all JDK Enhancement Proposals, known as JEPs.
See JEP 1 for an overview of the JEP Process.

Previewフェーズにある機能を利用する際はjavaおよびjavacコマンド実行時に--enable-previewオプションをつける必要がある。
https://docs.oracle.com/javase/jp/14/language/preview-language-and-vm-features.html

新機能

Project Amberの言語機能

JEP 455: プリミティブ型のパターン、instanceof、switch(プレビュー)

switch内で、以下のようにプリミティブ型を扱えるようになった

switch (x.getYearlyFlights()) {
    case 0 -> ...;
    case 1 -> ...;
    case 2 -> issueDiscount();
    case int i when i >= 100 -> issueGoldCard();
    case int i -> ... appropriate action when i > 2 && i < 100 ...
}

when構文はJava21のタイミングでswitchにいろいろと機能追加があった時に導入されたようだ。
case 型名 変数名で型が一致した場合に分岐に入ることができ(nullも指定可能)、そこがプリミティブ型に対応していなかったのがこのJEPで対応するということ。
whenで更に値によって条件指定が出来るのはguarded pattern case labelsというらしい。
switchは機能追加が頻繁に行われているので、久々にJavaを触る人でなくとも一度これまでのアップデートに目を通した方が良さそう。

instanceofについても同様にプリミティブ型のサポートが追加され

if (i >= -128 && i <= 127) {
    byte b = (byte)i;
    ... b ...
}

と書いていたような部分が

if (i instanceof byte b) {
    ... b ...
}

のように、キャスト可能かどうかを簡単に判別できるようになった。

JEP 476: モジュール・インポート宣言(プレビュー)

パッケージをまとめた単位としてmoduleというものがあり、module-info.javaに依存関係を書くことが出来るが、これをimportの単位として使えるということらしい。

import module java.baseとすると、java.io.* とか java.util.*を含んだ基本的なライブラリがimport出来る。

JEP 477: 暗黙的に宣言されたクラスとインスタンスのメイン・メソッド(第3プレビュー)

元々は別のJEPで、Java21ぐらいで話題になったmainメソッドだけでコンパイルが出来るというやつ。
修正が入って別のJEPになると第2とか第3プレビューになるようだ。

以前は

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

のように最小の実行単位としてクラス定義から書く必要があったが、

  • インスタンスメソッドとしてのmainの許可
  • 暗黙的なクラス宣言
  • java.baseの自動インポートによるSystem.out.の簡略化

によって

void main() {
    println("Hello, World!");
}

と書くことができ、初心者にわかりやすくなるよねという内容。

JEP 482: 柔軟なコンストラクタ本体(第2プレビュー)

コンストラクタ内でのsuper()やthis()の呼び出し前に文が書けるようになった。
今までは

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        super(value);                 // Potentially unnecessary work
        if (value <= 0) throw new IllegalArgumentException(..);
    }

}

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        if (value <= 0) throw new IllegalArgumentException(..);
        super(value);
    }

}

と書きたくても書けず、

public class PositiveBigInteger extends BigInteger {

    private static long verifyPositive(long value) {
        if (value <= 0) throw new IllegalArgumentException(..);
        return value;
    }

    public PositiveBigInteger(long value) {
        super(verifyPositive(value));
    }

}

のようなインラインでメソッドを挟むような回避策を取っていたのが不要になった、ということらしい。

ライブラリ

実際にコードを書く際の改善というよりは、内部的な改善やAPIの追加などが含まれるようだ

JEP 466: Class-File API (第2プレビュー)

Javaクラスファイルを解析、生成、変換するための標準APIとのこと。java.lang.classfileパッケージ。
正直JEPの文章を読んでもいまいちわからないが、

ClassFile cf = ClassFile.of();
ClassModel classModel = cf.parse(bytes);

としてクラスをパースすることで、メソッドやフィールドの情報を取得したり、既存クラスに処理の追加/削除した加工クラスのバイト列を生成できたりするようだ。
静的解析やメタプロに使う感じなのだろうか。

JEP 469: Vector API(第8インキュベーター)

第8という通りjava16の頃から形を変えて試験されているようだ。プレビューだと言語機能でインキュベーターはライブラリを指すらしい。

CPUアーキテクチャ上最適なベクトル命令(SIMD命令)にコンパイルされるようなベクトル計算を導入するものとのこと。
壮大な名前のProject ValhallaというJavaにプリミティブ型と参照型の中間みたいなもの(Value Types)を導入しようという長期プロジェクトがあり、ここに統合される目的で頑張っている模様。

JEP 473: Stream Gatherers (第2プレビュー)

Stream APIの中間操作の強化。
終端処理collectにCollectorsを渡すのと同じ感じで中間処理gatherメソッドが新設され、Gatherersクラスで自由な処理を書くことが出来るようになった。

JEPの例では、今まで

var result = Stream.iterate(0, i -> i + 1)
                   .windowFixed(3)
                   .limit(2)
                   .toList();

// result ==> [[0, 1, 2], [3, 4, 5]]

のような処理が書きたくても、無限ストリームに対して中間処理であるfilterなどでは書けず(終端処理まで処理が遅延するため)、終端処理で頑張って書くしかなかったのがgather内で書けるようになった。
今回のwindowFixedはcollect(Collectors.toList())みたいな感じで定義されていて

Stream.iterate(0, i -> i + 1)
    .gather(Gatherers.windowFixed(3))
    .limit(2)
    .toList();

のように書ける。
時系列データの前後の差分が大きいところだけfilterする、みたいな用途例が書いてあり色々使い道がありそう。

JEP 480: 構造化された並行性 (第3プレビュー)

構造化された平行性(Structured Concurrency)という概念自体はjava固有のものでなく、並行処理のパラダイムの一つであるようだ。

以下のように、2つのサブタスクを実行して両方の結果を返すhandle()があるとき

Response handle() throws ExecutionException, InterruptedException {
    Future<String>  user  = esvc.submit(() -> findUser());
    Future<Integer> order = esvc.submit(() -> fetchOrder());
    String theUser  = user.get();   // Join findUser
    int    theOrder = order.get();  // Join fetchOrder
    return new Response(theUser, theOrder);
}

findUser()、fetchOrder()、handle()のどこで失敗したのかによって、他のタスクが適切に中断されなかったり不要な待ちが発生したりする。
これはfindUser()とfetchOrder()が順に書かれているが実際には前後がわからないため起きる問題で、これを制御しようとするとcallbackを使って書けばわかるようになるが、
以下のようなシングルスレッドの書き方

Response handle() throws IOException {
    String theUser  = findUser();
    int    theOrder = fetchOrder();
    return new Response(theUser, theOrder);
}

と同じように制御されるようにするのが構造化された平行性の考え方で、callbackで頑張って書くのってgotoみたいなもんでわかりづらいよね、というのが出発点のようだ。

上記のパラダイムはjava.util.concurrent.StructuredTaskScopeによって実現されて

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Supplier<String>  user  = scope.fork(() -> findUser());
        Supplier<Integer> order = scope.fork(() -> fetchOrder());

        scope.join()            // Join both subtasks
             .throwIfFailed();  // ... and propagate errors

        // Here, both subtasks have succeeded, so compose their results
        return new Response(user.get(), order.get());
    }
}

のような、try-with-resourcesステートメントでscopeを区切ることによってキャンセルや失敗による挙動を(callbackを省いて)記述することが出来る。

JEP 481: スコープ値(第3プレビュー)

これもマルチスレッドでの処理の話。
従来はスレッドセーフな変数を用意するときにThreadLocal型があったが、いつでも変更可能なこと、有効期間が長いこと、スレッドが多くなるとコストがかさむ等の問題があり
特に仮想スレッドの導入によってスレッドの数が増える場合に備えて、軽量な共有のためにScopedValueが作られた。

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

    ScopedValue.runWhere(NAME, "duke", () -> doSomething());

のような形で、runWhereで値をバインドした後、渡されたメソッドの実行時のみバインドが行われて終了時にバインドが解除されるとのこと。

パフォーマンスおよびランタイムに関する更新

JEP 474: ZGC: 世代別モードのデフォルト化

ZGC(Z Garbage Collector)のデフォルト・モードを世代別モードに切り替えることで、2つの異なるモードをサポートするために必要なリソースと保守コストを削減し、開発者の効率を向上させることができます。

とある。ZGCはjava11の時から新機能として導入された高速なGCとのことで、これの動作モードとして世代別/非世代別モードがあり、新しい世代別モードがデフォルトになるよということ。
ここでいう「世代」は、GC対象となるオブジェクトを、オブジェクトの生存時間(寿命)に着目して管理することを言うようだ。

ツール

JEP 467: Markdownドキュメント・コメント

Markdown形式でjavaDocが書けるようになった。
従来の/**開始でなく、各行///で開始する必要があるようだ。
これによって内部にコードブロックを記述する際、行内に */ が使えるのでコメントを含められるようになった。

スチュワードシップ

JEP 471: sun.misc.unsafeのメモリアクセス・メソッドを削除を予定した非推奨に設定

スチュワードシップがわからず検索してみると「財産を管理するものの責任」みたいな意味合いのようだ。
メモリ操作を新しいForeign Function & Memory API (JEP 454)に統一し、unsafeを削除出来る形に持っていきたいということらしい。

Discussion