Java22 で追加されるマルチファイルコードの実行 (JEP 458 Launch Multi-File Source-Code Programs)

blog1.mammb.com


はじめに

Java11 では、JEP 330 Launch Single-File Source-Code Programs にて明示的なコンパイル操作を省略し、.java ファイルを java コマンドで直接実行できるようになりました。

例えば、Prog.java ファイルを以下の内容で作成した場合、

class Prog {
    public static void main(String[] args) { Helper.run(); }
}
class Helper {
    static void run() { System.out.println("Hello!"); }
}

単純に以下のように実行することができます(オンメモリ上でコンパイルして実行される)。

$ java Prog.java
Hello!

JEP 330 では、プログラムのソースコードはすべて1つの .java ファイルに収めなければならないという制限事項があります。

JEP 458 Launch Multi-File Source-Code Programs では、複数の .java ファイルに対しても、明示的なコンパイル操作なく実行を可能にします。


JEP 458 Launch Multi-File Source-Code Programs

あるディレクトリにProg.javaHelper.javaという2つのファイルがあり、Prog クラスが Helper クラスを参照しているとします。

// Prog.java
class Prog {
    public static void main(String[] args) { Helper.run(); }
}
// Helper.java
class Helper {
    static void run() { System.out.println("Hello!"); }
}

2つのファイルに分かれていても、以下のように実行することができるようになります。

$ java Prog.java
Hello!

java Prog.java を実行すると、Prog クラスがメモリ上でコンパイルされ、main メソッドが呼び出されます。このクラスのコードは Helper クラスを参照しているので、ランチャーはファイルシステム内の Helper.java ファイルを見つけて、そのクラスをメモリ内でコンパイルして実行します。


Using pre-compiled classes

クラス・パスまたはモジュール・パス上のライブラリに依存するプログラムも、ソース・ファイルから起動できます。

あるディレクトリに2つの小さなプログラムと1つのヘルパー・クラス、そしていくつかのライブラリJARファイルがあった場合、

Prog1.java
Prog2.java
Helper.java
library1.jar
library2.jar

以下のように --class-path '*' を指定して実行することができます(* はシェルによる展開を避けるために引用符で囲んでいる)。

$ java --class-path '*' Prog1.java
$ java --class-path '*' Prog2.java

ライブラリが libs ディレクトリにあった場合は --class-path 'libs/*' のようにクラスパスを指定できます。


モジュールライブラリ(ソースツリーのルートに module-info.java ファイルがある)を利用する場合は以下のように実行します。

$ java -p . pkg/Prog1.java

モジュール化されたJARファイルが libs ディレクトリにある場合は、-p libs のように指定できます。


How the launcher finds source files

java ランチャーは最初の .java ファイルのパッケージ名とファイルシステムの位置からソースツリーの ルートを計算します。

Prog が無名パッケージ、Helperpkg パッケージ(名前付きパッケージ)に属する場合は以下のように実行できます。

// Prog.java
class Prog {
    public static void main(String[] args) { pkg.Helper.run(); }
}
// pkg/Helper.java
package pkg;
class Helper {
    static void run() { System.out.println("Hello!"); }
}
$ java Prog.java
Hello!


Progmain パッケージに属する場合、

// main/Prog.java
package main;
class Prog {
    public static void main(String[] args) { pkg.Helper.run(); }
}
// pkg/Helper.java
package pkg;
class Helper {
    static void run() { System.out.println("Hello!"); }
}

以下のように実行する必要があります。

$ java main/Prog.java
Hello!

過去のリリースでは、java a/b/c/Prog.javaa/b/cProg.java がある限り、ファイル中の package の宣言に関係なく実行できましたが、本機能追加にてこの動作は変更されます。


Launch-time operation

javaランチャーのソースファイルモードは以下のステップで実行されます。

  1. ファイルが shebang #! 行で始まる場合、コンパイラに渡されるソースパスは空なので、ステップ4に進む(他のソースファイルはコンパイルされない)。
  2. ソースツリーのルートとなるディレクトリを計算する
  3. ソース・コード・プログラムのモジュールを決定する
    1. ルートに module-info.java ファイルが存在する場合は、そのモジュール宣言を使用して、ソースツリー内の .java ファイルからコンパイルされたすべてのクラスを含む名前付きモジュールを定義する
    2. もし module-info.java が存在しなければ、.java ファイルからコンパイルされたすべてのクラスは名前のないモジュールに格納される
  4. 最初の .java ファイル内のすべてのクラスと、場合によっては最初のファイル内のコードから参照されるクラスを宣言している他の .java ファイルをコンパイルし、結果の class ファイルをインメモリーキャッシュに格納する
  5. 初期ファイル .javalaunch class を決定する
    1. 初期ファイルの最初のトップレベルクラスが標準の main メソッド (public static void main(String[]) または JEP 463 で定義されているその他の標準 main エントリポイント) を宣言している場合、そのクラスが起動クラスとなる
    2. そうでない場合、初期ファイル内の他のトップレベルクラスが標準の main メソッドを宣言しており、ファイル名が同じであれば、そのクラスが起動クラスとなる
    3. そうでない場合、起動クラスは存在せず、ランチャーはエラーを報告して停止する
  6. カスタム・クラス・ローダーを使ってメモリ内のキャッシュから起動クラスをロードし、そのクラスの標準の main メソッドを呼び出す


Differences between compilation at compile time versus launch time

javac によるコンパイルと、java ランチャーのソースファイルモードで行われるコンパイルにはいくつかの大きな違いがあります。

  • ソースファイルモードでは、.javaファイルにあるクラス宣言は、実行開始前に一度にコンパイルされるのではなく、プログラムの実行中にオンデマンドでインクリメンタルにコンパイルされるため、コンパイル・エラーが発生した場合はプログラムの実行開始後に終了する
  • リフレクション経由でアクセスされたクラスは、直接アクセスされたクラスと同じ方法でロードされる
  • アノテーションプロセッサは無効化され、--proc:nonejavac に渡されたときと同じ動作となる
  • .java ファイルが複数のモジュールにまたがるソースコードプログラムを実行することはできない