JDK22では複数ファイルをjavacなしで実行できるようになるらしい

この記事は [ラクス Advent Calendar 2023](https://qiita.com/advent-calendar/2023/rakus) 12日目の記事です。

今年の9月に最新のLTSとなるバージョン21がリリースされたばかりのJavaですが、早くも来年3月にはバージョン22がリリース予定となっています。
いろいろとアップデートが入る予定ですが、今回はその中から、ランチャーの強化に関して取り上げたいと思います。

## 忙しい人向け

**javaコマンドで複数のjavaファイルにまたがる処理を実行できるようになります。**
具体的には以下のようなことができるようになります。

```java:Main.java
public class Main {
    public void main() {
        Dog dog = new Dog("hoge");
        System.out.println(dog.name());
    }
}
```

```java:Dog.java
public record Dog(String name) {}
```

```shell:javaコマンドで実行
$ java --enable-preview --source 22 Main.java
hoge
```

## これまでのJava

皆さんご存じの通り、JDK8までのJavaは基本的にjavacでコンパイルされたクラスファイルがないと実行できませんでした。
ちょっとお試しでコードを書いていろいろ動かしてみたいといった場合でも、いちいち`javacでコンパイル→javaで実行`というステップを踏む必要があったわけです。
（IDEを使ってたらあまり意識する必要もないかもですが。。。）

それがJDK9でJShellによるインタラクティブな実行が可能となり、JDK11で単一ファイルのコンパイルなし実行がサポートされました。
こうした動きによって、簡単にお試ししたいといったニーズを満たすとともに、初学者にもとっつきやすい環境を作るといった試みが、近年は盛んにおこなわれています。
（21でmainメソッド簡略化のプレビュー版が追加されたのも記憶に新しいですね）

## 複数ファイルのコンパイルなし実行がサポートされる背景

JDK11でサポートされたコンパイルなし実行ですが、あくまでも対象は単一ファイルです。
冒頭のプログラムを21で実行するとコンパイルエラーになります。

これに対してメスを入れて複数ファイルにまたがった実行を可能にしようというのが [JEP 458: Launch Multi-File Source-Code Programs](https://openjdk.org/jeps/458) なのですが、公式の説明によると、これをやる背景はざっくり以下のような感じでした。

* 単一ファイルに複数クラスを詰め込めば実行は可能だが、例えばいろいろいじってから本番コードに転用したくなった場合などに、後からクラスごとにファイルを分割するといった手間が発生する
* 複数ファイルを扱う場合は通常なんらかのビルドツールを使うことが多いが、例えば最初からお試しで捨てる前提の場合、捨てることがわかっているものに対していちいちビルドツールを導入するのは面倒
* 初学者がこのあたりのことを考えるのはかなりハードルが高い

たしかに、すぐ捨てるようなソースに対してmavenなりgradleなりを毎回いれるのは、ちょっと面倒な作業だったようにも思います。
（IDEを使えばイチコロですが、それも毎回同じことをやってるとちょっと面倒になってくる気がしたりしなかったり）

依存解決したいようなライブラリもなく、本当にさくっと作ってさくっと試したい（IDEのプロジェクト作成ウィザードすら面倒くさい）といった場合に、今回の変更は結構便利かもしれません。

## どうやって動いているのか

javaコマンドで指定するソースファイルのパッケージと配置ディレクトリからルートディレクトリを割り出し、そこを基準にファイルを探索しているようです。
冒頭の例は`Main.java`がデフォルトパッケージなので、`Main.java`が配置されているディレクトリがルートディレクトリとなります。

パッケージ指定する場合は、各ファイルがパッケージに沿ったディレクトリに正しく配置されている必要があります。
例えば冒頭の例を修正して、以下の通りパッケージを指定したとします。

```java:Main.java
package jp.co.test;

import jp.co.test.data.Dog;

public class Main {
    public void main() {
        Dog dog = new Dog("hoge");
        System.out.println(dog.name());
    }
}
```

```java:Dog.java
package jp.co.test.data;

public record Dog(String name) {}
```

この場合、ディレクトリ構成は以下のようになっている必要があります。
（当たり前っちゃ当たり前ですが・・・）

```:ディレクトリ構成
src/
    jp/
        co/
            test/
                Main.java
                data/
                    Dog.java
```

この時、ルートディレクトリは`src`となります。
これが例えば`Main.java`のパッケージを`co.test`に変更した場合は、ルートディレクトリが`jp`に変わります。
（その分ほかのjavaファイルのパッケージも修正する必要があります）

## パッケージ指定したファイルは、パッケージ通りに配置しないと動かなくなる

先ほどのファイル探索の仕様変更に伴い、パッケージが指定されたファイルについては、パッケージ通りのディレクトリに配置しないと動かなくなります。

例えば以下のようなファイルがあったとします。

```java:Test.java
package jp.co.test;

public class Test {
    public void main() {
        System.out.println("Hello World");
    }
}
```

```:ディレクトリ構成
src/
    Test.java
```

`jp.co.test`でパッケージ宣言されていますが、JDK21まではこれでも実行可能でした。
ですが22になると、以下のようなエラーが出るようになります。

```shell
$ java --enable-preview --source 22 Test.java
エラー: end of path to source file does not match its package name jp.co.test: Test.java
```

まあ、今までがゆるかったという面も否めないですかね。。。

## まとめ

といった感じで特にオチはありませんが、複数ファイルでもjavacをすっ飛ばして実行できるようになりますという話でした。

古来より何やらとっつきづらい印象が持たれがちなJavaですが、こうした取り組みによって随分と親しみやすくなってきているのではないでしょうか。

## ちなみに

現在、JDK22はアーリーアクセス中です。
残念ながら本記事執筆時点（2023/12/7）の最新版アーカイブにはJEP458が含まれていないため、お試ししたい場合はソースコードからビルドする必要があります。
（masterにはすでにマージされてました）

* ソース
  * https://github.com/openjdk/jdk.git
* ビルド手順
  * https://github.com/openjdk/jdk/blob/master/doc/building.md
  * TL;DRの項に記載の手順でいけました（Linux）

あとはいつも通りパスを通すだけ。

2023/12/11 追記
最新のビルドに追加されたようです！

https://jdk.java.net/22/