はじめに
コアテクノロジーグループの前川です。
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.javaimport 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) { 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) -> { 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.javapublic 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脳」から離れないといけなかったのがかなり軽減されます。 Integrator
の state
、遂に来たかという感じですね。
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) { List<List<Integer>> result = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9) .gather(Gatherers.windowFixed(3)) .toList();
System.out.println(result);
List<List<Integer>> result2 = Stream.of(1, 2, 3, 4, 5) .gather(Gatherers.windowSliding(3)) .toList();
System.out.println(result2);
List<Integer> result3 = Stream.of(1, 2, 3, 4, 5) .gather(Gatherers.scan(() -> 0, (sum, i) -> sum + i)) .toList();
System.out.println(result3);
List<List<String>> result4 = Stream.of("a", "a", "b", "c", "c", "c", "b", "a") .gather(groupConsecutive()) .toList();
System.out.println(result4); }
public static <T> Gatherer<T, ?, List<T>> groupConsecutive() { return Gatherer.ofSequential( ArrayList::new,
(state, element, downstream) -> { if (!state.isEmpty() && !state.getLast().equals(element)) { downstream.push(List.copyOf(state)); state.clear(); } state.add(element); return true; },
(state, downstream) -> { 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
クラスなどで利用できます。
KDF hkdf = KDF.getInstance("HKDF-SHA256");
AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract() .addIKM(initialKeyMaterial) .addSalt(salt).thenExpand(info, 32);
SecretKey key = hkdf.deriveKey("AES", params);
|
バージョン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のご紹介が続きます。