フューチャー技術ブログ

JDK25のアップデート

はじめに

こんにちは、TIGの岸本卓也です。 Java25リリース記念ブログ連載 シリーズの記事です。

本稿では以下の JDK 25 の変更点の内、プレビュー以外の言語機能に関する変更点を中心に内容を見ていきます。

JEP Title Category Sub category
470 PEM Encodings of Cryptographic Objects (Preview) Preview & Incubating Libraries
502 Stable Values (Preview) Preview & Incubating Libraries
503 Remove the 32-bit x86 Port Removals HotSpot JVM
505 Structured Concurrency (Fifth Preview) Preview & Incubating Libraries
506 Scoped Values Additions Libraries
507 Primitive Types in Patterns, instanceof, and switch (Third Preview) Preview & Incubating Language
508 Vector API (Tenth Incubator) Preview & Incubating Libraries
509 JFR CPU-Time Profiling (Experimental) Additions HotSpot JVM
510 Key Derivation Function API Additions Libraries
511 Module Import Declarations Additions Language
512 Compact Source Files and Instance Main Methods Additions Language
513 Flexible Constructor Bodies Additions Language
514 Ahead-of-Time Command-Line Ergonomics Additions HotSpot JVM
515 Ahead-of-Time Method Profiling Additions HotSpot JVM
518 JFR Cooperative Sampling Additions HotSpot JVM
519 Compact Object Headers Additions HotSpot JVM
520 JFR Method Timing & Tracing Additions HotSpot JVM
521 Generational Shenandoah Additions HotSpot JVM

503: Remove the 32-bit x86 Port

JEP 449 (JDK 21) 辺りから 32-bit x86 サポートの廃止に向けた動きが進んできていました。 Linux 向けの 32-bit x86 関連ソースが残るのみとなっていましたが、32-bit x86 サポートのために発生していた足かせの排除やビルド・テストの簡素化のために JEP 503 にてソースが削除されました。

506: Scoped Values

JEP 429 (JDK 20) からプレビュー提供されていた Scoped Values が、プレビューを終えて正式リリースされました。本機能は主に ScopedValue クラスにより提供され、次のような場合に役立ちます。

メソッドにデータを渡す方法として、メソッド引数で渡す方法があります。メソッド引数は単純で分かりやすい方法ですが、メソッド内でさらに別のメソッドを呼び出して…といった構成で実際にデータを使うのが深い階層のメソッドである場合、各メソッド呼び出しに引数を定義してデータをバケツリレーする必要があります。メソッド引数で渡す方法は、中間のメソッドではそのデータを使わないとしてもバケツリレーのために引数を定義する必要があるという点が課題です。

これを解決する方法として ThreadLocal がよく使われます。例えば、

  1. Framework がコンテキスト情報を生成する
  2. そのコンテキスト情報のもとで Framework が Application を実行する
  3. Application が Framework の機能を呼び出す
  4. 呼び出された Framework 機能がコンテキスト情報を使う

といった構成において、コンテキスト情報を ThreadLocal で渡す実装例は以下です。

ThreadLocalExample.java
public class ThreadLocalExample {
void main() {
var fw = new Framework(new Application());
fw.serve("Java");
fw.serve("Future");
}
}

class Framework {
private static final ThreadLocal<FrameworkContext> CONTEXT = new ThreadLocal<>();

private final Application application;

public Framework(final Application application) {
this.application = application;
}

public void serve(final String request) {
var context = new FrameworkContext(request);
CONTEXT.set(context);
application.handle();
CONTEXT.remove();
}

public static void greet() {
IO.println("Hello %s!".formatted(CONTEXT.get().getName()));
}
}

class FrameworkContext {
private String name;

public FrameworkContext(final String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

class Application {
public void handle() {
Framework.greet();
}
}
C:\>java ThreadLocalExample.java
Hello Java!
Hello Future!

この実装は期待通り動きますが、 ThreadLocal の方法には以下の課題があります。

  • 自由に変更できる: ThreadLocal にアクセスできるコードからは set メソッドにより自由に値が変更できます。いつ、どこから変更されるかわからない変数の管理は難しく、バグを生みがちです。
  • 無制限の存続期間: ThreadLocalset された値は、スレッドが存続する間または remove メソッドが呼ばれるまで保持されます。 remove メソッド呼び出しは忘れがちなため不必要に長期間変数が生存する可能性があり、スレッドプールを使用する場合は意図せずリークして脆弱性やメモリリークに繋がります。
  • 継承が高コスト: 親スレッドのスレッドローカル変数を継承して子スレッドを作成できますが、全てのスレッドローカル変数に対する領域をを割り当てる必要があるため多くのメモリが必要となる可能性があります。

このような課題を解決するため、 ThreadLocal よりも汎用性を減らし用途を絞った新たな API が ScopedValue です。 ScopedValue により、同一スレッド内または子スレッドとの間でより安全かつ効率的にデータを共有できます。

先程の Framework-Application の例を ScopedValue に変更した実装例は以下です。

ScopedValueExample1.java
public class ScopedValueExample1 {
void main() {
var fw = new Framework(new Application());
fw.serve("Java");
fw.serve("Future");
}
}

class Framework {
// バインドされていない (i.e. 空の) scoped value は `newInstance` メソッドで作成する
private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();

private final Application application;

public Framework(final Application application) {
this.application = application;
}

public void serve(final String request) {
var context = new FrameworkContext(request);
ScopedValue
// scoped value に値をバインドする (i.e. セットする) には `where` メソッドを使う
.where(CONTEXT, context)
// 値がバインドされた状態で実行したいコードは `run` メソッドで実行する
.run(() -> application.handle());
}

public static void greet() {
IO.println("Hello %s!".formatted(CONTEXT.get().getName()));
}
}

// FrameworkContext と Application は先の例と同じため記載省略
C:\>java ScopedValueExample1.java
Hello Java!
Hello Future!

ScopedValue のスコープは以下のようにネストする (rebinding) こともできます。

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

void main() {
ScopedValue.where(NAME, "Java").run(this::doSomething1);
}

private void doSomething1() {
greet("doSomething1");

// `ScopedValue` のネスト
ScopedValue.where(NAME, "Future").run(this::doSomething2);

// ネストを抜けると scoped value は元の値に戻る
greet("doSomething1");
}

private void doSomething2() {
greet("doSomething2");
}

private void greet(final String from) {
IO.println("%s: Hello %s!".formatted(from, NAME.get()));
}
C:\>java ScopedValueExample2.java
doSomething1: Hello Java!
doSomething2: Hello Future!
doSomething1: Hello Java!

ThreadLocal の場合は値を変更すると変更されたままです。

ThreadLocalExample2.java
private static final ThreadLocal<String> NAME = new ThreadLocal<>();

void main(final String[] args) {
NAME.set("Java");
doSomething1();
}

private void doSomething1() {
greet("doSomething1");

NAME.set("Future");
doSomething2();

greet("doSomething1");
}

private void doSomething2() {
greet("doSomething2");
}

private void greet(final String from) {
IO.println("%s: Hello %s!".formatted(from, NAME.get()));
}
C:\>java ThreadLocalExample2.java
doSomething1: Hello Java!
doSomething2: Hello Future!
doSomething1: Hello Future!

511: Module Import Declarations

モジュール単位でインポートできる仕組みです。これまでインポートの宣言方法は single-type-import と type-import-on-demand の2種類でした。それぞれの例は以下です。

single-type-import

import java.util.Map;

type-import-on-demand

import java.util.*;

新たに追加された module import によりモジュール単位でのインポートが可能になります。

import module java.base;

同名クラスが存在する場合はどのクラスをインポートするか曖昧になります。そのような場合は single-type-import または type-import-on-demand のインポート宣言を追加 (shadowing) して曖昧さを解決します。

import module java.base;    // java.util.Date が含まれる
import module java.sql; // java.sql.Date が含まれる

import java.util.*; // type-import-on-demand で曖昧さを解消する例

なお、当社が公開している Java コーディング規約 もそうですが、インポート宣言は明確さを優先して single-type-import が好まれることが多いです。しかし、 JShell 使用時やお試し実装など明確さより便利さを優先できる場合には有用になりそうです。

512: Compact Source Files and Instance Main Methods

おまじない/ボイラープレート少なく Java プログラムを書き始められる仕組みです。プレビューでは simple source files と呼ばれていましたが compact source files という名称に変更して正式リリースされました。

これまでは例えば Hello, World! のコードは以下のように実装する必要がありました。

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

このコードには Hello, World! を出力するメインのコード以外に多くのコードが含まれているため、 Java 初学者を混乱させる可能性があります。

compact source files と instance main methods の仕組みを使うと、 Hello, World! のコードは以下にできます。

// class 定義を省略できる
// main メソッドの修飾子を省略できる
// main メソッドの引数を省略できる
void main() {
// `java.lang` パッケージに新たに追加された IO クラスを使う
IO.println("Hello, World!");
}

class 定義に囲まれていないフィールドやメソッドを含むソースを compact source file と呼び、それらのフィールドやメソッドをメンバーとする暗黙のクラスが定義されているように扱われます。暗黙に定義されるクラスは名前を持たないため、 new 演算子でインスタンス化することや static メソッドのメソッド参照はできません。しかし、後述の仕組みで作られるインスタンスを this で参照できます。

暗黙定義されるクラスは次の仕様になります。

  • 無名パッケージに定義されたトップレベルの final クラス
  • Object クラスを継承し、インターフェース実装は無し
  • コンストラクタは引数無しのデフォルトコンストラクタのみ存在する

main メソッドは次の仕組みで選択されます。

  1. クラスに直接定義または継承で String[] を引数とする main メソッドがあればそのメソッドを選択する。
  2. クラスに直接定義または継承で引数無しの main メソッドがあればそのメソッドを選択する。

選択された main メソッドは次の仕組みで起動されます。

  1. 選択されたメソッドが static ならそのメソッドを起動する。
  2. 選択されたメソッドがインスタンスメソッドなら、引数無しのコンストラクタを起動してインスタンスを作成してから選択されたメソッドを起動する。

この簡潔な実装方法は 506: Scoped Values の実装例でもしれっと使っていました。

また、 compact source files では java.base モジュールが自動でインポートされるため、以下のようによく使うクラスもインポート宣言無しに使えます。

void main() {
var fruits = new String[] { "apple", "berry", "citrus" };
var m = Stream.of(fruits).collect(Collectors.toMap(
s -> s.toUpperCase().substring(0,1),
Function.identity()));
m.forEach((k, v) -> IO.println(k + " " + v));
}

513: Flexible Constructor Bodies

コンストラクタの処理でコンストラクタ呼び出し (super(...)this(...)) より前に文 (statement) を置けるようになりました。これにより、例えば以下のガード節のような実装が可能になります。

class A {
A(final int value) {
if (value < 0) {
throw new IllegalArgumentException();
}

super(value);
}
}

なお、コンストラクタ呼び出しより前では thissuper などで作成中のインスタンスの参照はできず初期化のみできます。例えば以下はコンパイルエラーとなる実装です。

class A {
int i;
A() {
// エラー: スーパータイプのコンストラクタの呼出し前はthisを参照できません
this.i++;
// エラー: スーパータイプのコンストラクタの呼出し前はiを参照できません
i++;
// エラー: スーパータイプのコンストラクタの呼出し前はthisを参照できません
this.hashCode();

super();

// コンストラクタ呼び出し後の参照はOK
this.i++;
this.hashCode();
}
}

コンストラクタ呼び出しより前のインスタンスアクセスは初期化のみ可能です。

class A {
int i;
A(final int value) {
// 初期化はできる
this.i = value;
super();
}
}

さいごに

本稿では JDK 25 の言語機能系の変更点をピックアップして紹介しました。

今回始めて Java のリリース内容を調査しましたが、 JDK 25 のページ やここからリンクされている各 JEP のページ、Oracle 社の Updates ページ は説明やサンプルが多くとても参考になりました。