コア・ライブラリとツールの主な変更点
変更の多くは、Project Amber/Panama/Loomに関わるものですが、ここでは、それら以外の主にJava APIのコアライブラリやツールにおける変更点について紹介します。
JEP 458: マルチファイル・ソースコード・プログラムの起動
Javaでは多くの場合、ビルド(コンパイル)処理をしてから実行するというのが通常でした。しかし、Java11からは単一のソースコードに限り、コンパイルしなくてもそのまま実行する事が可能です。これは、簡単なサンプルコードを実行する場合やちょっとした機能を確認する場合には有用でした。
しかし、Javaプロジェクトでよくあるクラス毎のファイル構成にしようとすると、結局、ビルドが必要になってしまいます。そこでJava22からは、これらの制限が撤廃され、単一ソースコードに限らず、複数のファイルで構成されているプログラムもコンパイル無しに実行できるようになりました。
例えば、リスト1のような同等のパッケージ名とクラス名をもつファイルがあるとします。
├── Exec.java ├── Jep458.java ├── main │ └── Main.java └── sub └── SubTask.java
そして、パッケージ名を持たないクラスであるリスト2を作成します。
class Jep458{ public static void main(String[] args){ Exec.run(); } }
同様に、パッケージ名をもつクラスであるリスト3を作ります。
package main; class Main{ public static void main(String[] args) { sub.SubTask.exec(); } }
このどちらのファイルを実行する場合でもリスト4のように実行が可能です。
$java Jep458.java $java main/Main.java
もちろん、--class-pathなどのオプションも使えますので、既存のライブラリと合わせても使えます。従って、gradleやmavenのようなビルドツールを導入するほどではないレベルでは、これまでよりも格段にJavaコードの実行が簡単になります。
JEP 467: Markdown形式でのJavaコメント
Javadocを用いて出力するJavaのコメント記述においてMarkdown形式での記述ができるようになりました。これまでは、リスト5のように/**〜*/でコメントを記述していました。
/** * このプログラムに関するコメントです。 * <ul> * <li>リスト1</li> * <li>リスト2</li> * </ul> * @see <a href="https://xxxxxx/yyyy.html">リンク</a> */
これがリスト6のように記述できるようになりました。Markdown形式の場合には///(スラッシュが3つ)で始まるようにしました。
/// /// このプログラムのMarkdown形式でのコメントです。 /// /// - リスト1 /// - リスト2 /// /// @see <https://xxxxxx/yyyy.html> /// @see <a href="https://xxxxxx/yyyy.html">リンク</a> ///
最近では開発者が使うさまざまなツールにおいてMarkdown形式がサポートされていることが多いため、Markdown形式の記述を好む開発者も増えてきたと思います。そのような開発者にとっては良い選択肢ができたと言えます。
ただし、実際には多くの開発者がIDEなどを使ってプログラムを記述していると思いますので、そちらの対応待ちという事になるのではないかと思います。また、現時点では必ずしも一般的なMarkdown形式で使える記述方法がすべて使えるわけではない点も注意が必要です。
JEP 461,473:Stream処理における中間操作での集約API
JavaのStream処理において、各要素に対する集約を行うための中間操作が出来るようになりました。Streamは、リスト7のように3つの行程で処理を記述します。
var result = Stream.iterate(0, i -> i + 1) // (1) Streamの作成 .filter(e -> e % 2 == 0).limit(5) // (2) 中間操作 .toList(); // (3) 終端操作
(1)でStreamを作成し、そして、(2)での中間処理。そして、最後に(3)の終端処理です。中間処理では、複数の中間処理を連結して各要素に対して変換や条件などにより抽出などができました。
そして、今回導入のAPIによって、この中間処理で要素に対して集約の処理が出来るようになりました。例えば、リスト8は指定した要素数毎の集約を行う処理の実装例です。
var result = Stream.of(1,2,3,4,5,6,7,8,9,10) .gather(Gatherers.windowFixed(2)) .toList(); // [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
また、指定した要素数毎に1つずつ値をスライドさせていくような集約の実装もあります。(リスト9)
var result = Stream.of(1,2,3,4,5,6) .gather(Gatherers.windowSliding(3)) .toList(); // [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]]
また、独自に集約処理を実装する場合には、Gathererインターフェースを実装します。リスト10は、Gatherers.windowFixedと同様の処理をする実装コード例です。
record WindowFixed<TR>(int windowSize) implements Gatherer<TR, ArrayList<TR>, List<TR>> { public WindowFixed { if (windowSize < 1) throw new IllegalArgumentException("window size must be positive"); } @Override public Supplier<ArrayList<TR>> initializer() { // (1) 初期化処理 return () -> new ArrayList<>(windowSize); } @Override public Integrator<ArrayList<TR>, TR, List<TR>> integrator() { // (2) 各要素の処理 return Gatherer.Integrator.ofGreedy((window, element, downstream) -> { // (3) windowは初期処理(1)のオブジェクト,elementは各要素,downstreamはストリーム window.add(element); if (window.size() < windowSize) { // サイズが十分でないので、後の処理はしない return true; } // 処理の単位が満たされたので、ストリームに書き出す var result = new ArrayList<TR>(window); window.clear(); return downstream.push(result); }); } @Override public BiConsumer<ArrayList<TR>, Downstream<? super List<TR>>> finisher() { // (4) ストリームの終端処理 return (window, downstream) -> { // (5) 残ったデータがあれば、それをストリームに書き出す if (!downstream.isRejecting() && !window.isEmpty()) { downstream.push(new ArrayList<TR>(window)); window.clear(); } }; } }
まず、大きな流れとして(1)で初期化処理を実装し、(2)で各要素での処理、(4)でストリームの終端となる処理を実装します。
そして、指定された要素毎のグループに変換するので、(3)処理に必要な各オブジェクトが引数から取得できるので、それらを使って指定したまとまりへと変換します。
また、最後に残ったデータがある場合には、それを(5)のようにストリームに書き出します。
この定義を使う場合には、リスト11のようになります。
var result = Stream.of(1, 2, 3, 4, 5, 6,7) .gather(new WindowFixed<>(3)) .toList();
また、Gatherer.ofSequential()メソッドを使えば、Gathererインターフェースの定義を別に行わず、先ほどの処理をリスト12のように書き換える事が出来ます。
var result = Stream.of(1, 2, 3, 4, 5, 6) .gather(Gatherer.ofSequential( () -> new ArrayList<>(), Gatherer.Integrator.ofGreedy((window, element, downstream) -> { // (省略) }), (window, downstream) -> { // (省略) })) .toList();
JEP 457,466:クラスファイル API
Javaではクラスファイルのバイトコードを解析し、それに応じて任意の処理を挿入、もしくは変更してしまうケースがあります。例えば、特定のメソッドを実行する場合、自動的に何らかの事前処理を強制的に差し込んだり、または実装を他の内容に書き換えたりという事です。
このAPIはClassなどのリフレクションを用いたものではなく、バイトコード(.classファイルの中身)から構造を解析、変更する為のAPIです。このような事をする場合には、ASMやJavaassistなどがありますが、本APIで同様な事ができるようになりました。
ただし、これらのライブラリを置き換える為のAPIというよりは、これらのライブラリにかかわる開発者に向けたAPIと言えると思います。
例えば、リスト13のような簡単なクラスを例に説明します。
package sub; public class Sub { private static int DEF_VALUE = 10; // 常に10を返す public int constantValue(){ return DEF_VALUE; } }
このクラスから生成したバイトコードを読み込み、フィールドの情報やメソッドの情報を読み取る為のサンプルコードがリスト14です。
ClassFile cf = ClassFile.of(); File file = new File("java/jep466/sub/Sub.class"); byte[] fileContent = Files.readAllBytes(file.toPath()); // (1) バイトコード情報を読み取る ClassModel classModel = cf.parse(fileContent); // (2) フィールドやメソッドの情報などを取得する for (FieldModel fm : classModel.fields()) System.out.printf("Field %s%n", fm.fieldName().stringValue()); for (MethodModel mm : classModel.methods()) System.out.printf("Method %s%n", mm.methodName().stringValue());
(1)でバイト情報を読み込み、そして、(2)でフィールドやメソッドの情報を取得しています。また、実行した結果がリスト15です。
Field DEF_VALUE Method <init> Method constantValue Method <clinit>
そしてリスト16は、先ほどのSubクラスにおけるconstantValueメソッドの実装部分を変更する場合の実装例です。
// (省略) // (1) バイトコード情報を読み取る ClassModel classModel = cf.parse(fileContent); // (2) クラス構造の読み込みと書換 byte[] newBytes = cf.transform(classModel, (classBuilder, ce) -> { if (ce instanceof MethodModel mm) { // (3) メソッドの定義部分 if(mm.methodName().equalsString("constantValue")){ // (4) 変更するメソッドの書換 classBuilder.transformMethod(mm, (methodBuilder, me)-> { if(me instanceof CodeModel cm){ // (5) コードを書換 methodBuilder.transformCode(cm, (codeBuilder, e) -> { switch (e){ case FieldInstruction c -> { // (6) 定数ではなく、指定した数字を返すように変更する codeBuilder.bipush(15); } default -> { codeBuilder.with(e); } } }); } else{ methodBuilder.with(me); } }); } else{ classBuilder.with(mm); } } else{ classBuilder.with(ce); } });
(1)でバイトコードの内容を読み込みます。そして、(2)でクラス構造の読み込みと書換を行うtransformメソッドを使います。(3)対象となるメソッドの時に、(4)のようにメソッド構造の読み込みと書換を行う処理を実行し、(5)続いてメソッド内の読み込みと書換を行います。
そして、(6)が本来static fieldのDEF_VALUEを返していたところに15という値を返すように書き換えています。
今回のコードはあくまでJavaのバイトコードが書き換えできるという一例を示したに過ぎません。実用的なコードではありませんので、ご注意ください。
最後に
Java22およびJava23はLTS版ではなく、新機能のお試しリリースという位置づけが大きいためPreview版での機能が多くなっています。
ただし、主にツールに関する部分(javaやjavadocコマンドなど)は正式リリースとなっていて、これらの影響が各関連ツール(IDEなど)に反映されTLS版がリリースされる頃には実際の開発者が触っているツールでも問題なく使えるようになっているはずです。
次回は、Java開発者にとって最も大きな影響と思われるProject Amber関連の変更点を中心に紹介します。