フューチャー技術ブログ

JDK24

はじめに

コアテクノロジーグループの前川です。

JDK 25のリリース連載 1本目の記事です。

今回はJDK 24でのアップデート内容から以下についてピックアップしてご紹介します。

JEP 484: Class-File API (クラスファイルの解析、生成、変換を行うための標準API)

Java バイトコードを読み込んでクラスファイルの解析、生成、変換を行うための機能を公式に提供するためのライブラリとしては、ASM や BCEL などが広く使われていますが、これらは JDK の一部ではなく、外部ライブラリとしてプロジェクトに組み込む必要があります。JEP 484 では、こうした外部ライブラリに依存せずに、JDK 標準でクラスファイルの解析、生成、変換を行うための API を提供します。 JDK 22から2回のプレビューを経て正式リリースとなりました。

以下は ClassBuilder を使用して既存のクラスのメソッド冒頭に System.out.println を仕込んで新クラスのclassファイルを作成するサンプルです。

ClassRenamer.java
import java.lang.classfile.*;
import java.lang.constant.*;
import java.nio.file.*;
import java.io.IOException;

public class ClassRenamer {

public static void main(String[] args) throws IOException {
Path sourcePath = Path.of("MyTargetClass.class");
String newClassName = "MyTargetClass2";
Path targetPath = Path.of(newClassName + ".class");

// 元のクラスファイルを解析
byte[] classBytes = Files.readAllBytes(sourcePath);
ClassModel originalModel = ClassFile.of().parse(classBytes);

// 新しいクラス名でクラスビルダーを開始し、元のクラスの全要素を新しいビルダーにコピーする
byte[] newClassBytes = ClassFile.of().build(ClassDesc.of(newClassName), builder -> {
for (ClassElement element : originalModel) {
// 要素がメソッドの場合、コードを変換して追加
if (element instanceof MethodModel method) {
// withMethod で新しいメソッドを構築
builder.withMethod(method.methodName(), method.methodType(), method.flags().flagsMask(), methodBuilder -> {
// 元のメソッドの属性(アノテーションなど)をコピー
for (MethodElement me : method) {
if (!(me instanceof CodeModel)) {
methodBuilder.with(me);
}
}

boolean[] inserted = {false};
methodBuilder.transformCode(
method.code().orElseThrow(),
(codeBuilder, codeElement) -> {
// フラグが false の場合(=初回実行時)のみ挿入
if (!inserted[0]) {
insertPrintStatement(codeBuilder, ">> Entering method: " + method.methodName().stringValue());
inserted[0] = true;
}
codeBuilder.with(codeElement);
});
});
} else {
builder.with(element);
}
}
});
// 変換後のバイトコードを新しいファイルに書き出す
Files.write(targetPath, newClassBytes);

System.out.println("クラス名を '" + newClassName + "' に変更し、'" + targetPath + "' に保存しました。");
}

private static final ClassDesc SYSTEM = ClassDesc.of("java.lang.System");
private static final ClassDesc PRINT_STREAM = ClassDesc.of("java.io.PrintStream");
private static final MethodTypeDesc PRINTLN_MTD = MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_String);

private static void insertPrintStatement(CodeBuilder codeBuilder, String message) {
codeBuilder.getstatic(SYSTEM, "out", PRINT_STREAM)
.ldc(message)
.invokevirtual(PRINT_STREAM, "println", PRINTLN_MTD);
}
}
MyTargetClass.java
public class MyTargetClass {
public static void main(String[] args) {
System.out.println("Hello!");
}
}
$ javac *.java
$ java -cp . ClassRenamer
クラス名を 'MyTargetClass2' に変更し、'MyTargetClass2.class' に保存しました。
$ java -cp . MyTargetClass2
>> Entering method: main
Hello!

考え方としてはASMに近いですが記法が異なっていて、ASMがビジターパターンなのに対してこちらはビルダーの多層構造になっており、 OpenRewrite 等でサクッと移行というわけにはいかなさそうです。

JEP 485: Stream Gatherers (Streamでより柔軟な中間操作を可能にするgatherメソッド)

Streamパイプラインの中に、自由度の高いカスタム中間操作を組み込むことを可能にします。これにより、開発者はこれまで以上に柔軟かつ直観的にデータ処理を記述できるようになります。

Collector で頑張ろうとすると一旦「Stream脳」から離れないといけなかったのがかなり軽減されます。 Integratorstate 、遂に来たかという感じですね。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Gatherer;
import java.util.stream.Gatherers;
import java.util.stream.Stream;

public class StreamSample {
public static void main(String[] args) {
// 1から9までの数値を3つずつのリストにまとめる
List<List<Integer>> result = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.gather(Gatherers.windowFixed(3))
.toList();

// 実行結果: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
System.out.println(result);

// 1から5までの数値を、3つずつのスライディングウィンドウでまとめる
List<List<Integer>> result2 = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(3))
.toList();

// 実行結果: [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
System.out.println(result2);

// 数値の累積和を計算する
List<Integer> result3 = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.scan(() -> 0, (sum, i) -> sum + i))
.toList();

// 実行結果: [1, 3, 6, 10, 15]
System.out.println(result3);

// カスタムGathererを使用
List<List<String>> result4 = Stream.of("a", "a", "b", "c", "c", "c", "b", "a")
.gather(groupConsecutive())
.toList();

// 実行結果: [["a", "a"], ["b"], ["c", "c", "c"], ["b"], ["a"]]
System.out.println(result4);
}

/** Stream内の連続する同じ要素をリストにグループ化するGathererを作成 */
public static <T> Gatherer<T, ?, List<T>> groupConsecutive() {
return Gatherer.ofSequential(
// 1. Initializer: 中間状態を保持するコンテナを準備する
ArrayList::new,

// 2. Integrator: 各要素を処理する
(state, element, downstream) -> {
// stateが空でなく、最後の要素が現在の要素と違う場合、これまでのstateをリストとして下流に流し、stateをクリアする
if (!state.isEmpty() && !state.getLast().equals(element)) {
downstream.push(List.copyOf(state));
state.clear();
}
// 現在の要素をstateに追加する
state.add(element);
return true;
},

// 3. Finisher: 最後の要素グループを処理する
(state, downstream) -> {
// Streamの最後に残った要素がstateにあれば、それを下流に流す
if (!state.isEmpty()) {
downstream.push(List.copyOf(state));
}
});
}
}

JEP 478: Key Derivation Function API (鍵導出関数のためのAPI)

ポスト量子暗号(PQC)対応の一環。

鍵導出関数(KDF)をひとことで言うと、 「鍵の”おおもと”(マスターキーやパスワード)から、用途に合わせて安全な”子鍵”を複数生成するための仕組み」 です。これまでJavaには鍵導出を行う為の統一的な標準APIが有りませんでした。この状況を改善し、標準化と相互運用性を高めてよりモダンで堅牢なアルゴリズムの利用を促進する事を目的として導入されたのが javax.crypto.KDF クラスです。

例はJEPのページからそのまま抜粋しますが以下のコードにより javax.crypto.SecretKey オブジェクトを生成できます。 SecretKey オブジェクトは従来と同様に Cipher クラスなどで利用できます。

// Create a KDF object for the specified algorithm
KDF hkdf = KDF.getInstance("HKDF-SHA256");

// Create an ExtractExpand parameter specification
AlgorithmParameterSpec params =
HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial)
.addSalt(salt).thenExpand(info, 32);

// Derive a 32-byte AES key
SecretKey key = hkdf.deriveKey("AES", params);

// Additional deriveKey calls can be made with the same KDF object

バージョン24時点ではPreviewなので、コンパイル時と実行時に以下のオプションを指定する必要があります。

$ javac --release 24 --enable-preview Foo.java
$ java --enable-preview Foo

# または

$ java --enable-preview Foo.java

# または

$ jshell --enable-preview

JEP 472: Prepare to Restrict the Use of JNI (JNIの安全でない使用を制限)

このJEPは、Java Native Interface の安全でない使用を将来的に制限するための準備です。ネイティブコードがJVMの整合性を損なう可能性のあるJNI関数を呼び出した際に、デフォルトで警告が表示されるようになりました。

JEP 486: Permanently Disable the Security Manager (セキュリティマネージャを恒久的に無効化)

Java 17で非推奨となっていたセキュリティマネージャが、このバージョンでデフォルトで無効化されました。まだ完全な削除ではなく、コマンドラインオプションで有効化する事が可能です。

JEP 496 & 497: Quantum-Resistant (量子コンピュータによる攻撃に耐性)

これら2つのJEPは、将来の量子コンピュータによる攻撃に耐えうる暗号技術を導入するものです。JEP 496では暗号通信のための鍵カプセル化メカニズム、JEP 497ではデジタル署名アルゴリズムが実装されました。

JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe (sun.misc.Unsafeのメモリ操作メソッド使用時に警告)

sun.misc.Unsafe クラス内の特定のメモリ操作メソッドが使用された際に、警告が発せられるようになりました。これらのメソッドはJVMを不安定にするリスクがあるため、開発者には公式にサポートされている安全なAPIへの移行が推奨されています。

おわりに

「ポスト量子」の世界がいよいよ現実味を増してきましたね。

次回は引続きJDK 24のご紹介が続きます。