CLOVER🍀

That was when it all began.

javacの--releaseオプションについて(JEP 247 Compile for Older Platform Versions)

これは、なにをしたくて書いたもの?

javacコマンドで、--releaseというオプションがあることを認識していなかったようなので、少し見ておくことにしました。

これは、JEP 247 Compile for Older Platform Versionsというもののようです。

JEP 247 Compile for Older Platform Versions

JEP 247(Compile for Older Platform Versions)は、Java 9で追加されたものです。

JEP 247: Compile for Older Platform Versions

似たようなオプションに-sourceと-targetがあります。こちらを使った時の問題点として、-targetで指定したバージョンよりも
新しいAPIを使ってもコンパイルが通ってしまい、実際に古いバージョンのJavaで実行した時に実行時エラーになるということがありました。

Java 9で追加された--releaseというオプションを使うことで、指定したバージョンのJavaで使えるAPIの範囲でコンパイルすることが
できるようになります。改良された-sourceと-targetという感じですね。

以下はjavac --helpによる説明ですが、--releaseと-source、-targetで指定できる値は同じですね。

  --release <release>
        指定されたJava SEリリースに対してコンパイルします。サポートされているリリース:
            8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21


  --source <release>, -source <release>
        指定されたJava SEリリースとソースの互換性を保持します。サポートされているリリース:
            8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21


  --target <release>, -target <release>
        指定されたJava SEリリースに適したクラス・ファイルを生成します。サポートされているリリース:
            8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21

ちょっと試してみましょう。

環境

今回の環境は、こちら。

$ javac --version
javac 21.0.1


$ java --version
openjdk 21.0.1 2023-10-17
OpenJDK Runtime Environment (build 21.0.1+12-Ubuntu-222.04)
OpenJDK 64-Bit Server VM (build 21.0.1+12-Ubuntu-222.04, mixed mode, sharing)

また、Java 8も利用します。

$ /usr/lib/jvm/java-8-openjdk-amd64/bin/javac -version
javac 1.8.0_392


$ /usr/lib/jvm/java-8-openjdk-amd64/bin/java -version
openjdk version "1.8.0_392"
OpenJDK Runtime Environment (build 1.8.0_392-8u392-ga-1~22.04-b08)
OpenJDK 64-Bit Server VM (build 25.392-b08, mixed mode)

最後の方で、Mavenも使います。

$ mvn --version
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-91-generic", arch: "amd64", family: "unix"

-sourceと-targetで確認する

まずは-sourceと-targetで確認してみましょう。

たとえば、List.ofはJava 9で追加されたメソッドです。

こちらを使ったソースコードを作成。

App.java

import java.util.List;

public class App {
    public static void main(String... args) {
        System.out.println(List.of(1, 2, 3));
    }
}

Java 8としてコンパイルします。

$ javac --source 8 --target 8 App.java
警告: [options] ブートストラップ・クラスパスが-source 8と一緒に設定されていません
警告: [options] ソース値8は廃止されていて、今後のリリースで削除される予定です
警告: [options] ターゲット値8は廃止されていて、今後のリリースで削除される予定です
警告: [options] 廃止されたオプションについての警告を表示しないようにするには、-Xlint:オプションを使用します。
警告4個

コンパイルは通ります。

で、実際にJava 8で実行すると実行時エラーになります。

$ /usr/lib/jvm/java-8-openjdk-amd64/bin/java App
Exception in thread "main" java.lang.NoSuchMethodError: java.util.List.of(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/List;
        at App.main(App.java:5)

ちなみに、警告に出ていますが--boot-class-path(-bootclasspath)を合わせて指定するとコンパイルエラーにすることができます。

$ javac --source 8 --target 8 --boot-class-path /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/rt.jar App.java
警告: [options] ソース値8は廃止されていて、今後のリリースで削除される予定です
警告: [options] ターゲット値8は廃止されていて、今後のリリースで削除される予定です
警告: [options] 廃止されたオプションについての警告を表示しないようにするには、-Xlint:オプションを使用します。
App.java:5: エラー: シンボルを見つけられません
        System.out.println(List.of(1, 2, 3));
                               ^
  シンボル:   メソッド of(int,int,int)
  場所: インタフェース List
エラー1個
警告3個

一応、--boot-class-pathを使うことで整合性を取れるようにはできるようですが、設定が多いのとrt.jarをパス指定する必要があるのが
厄介ですね…。

--releaseオプションを使う

それでは、--releaseオプションを使ってみましょう。

先ほどのソースコードを--releaseオプションをつけて、Java 8としてコンパイルしてみます。

$ javac --release 8 App.java
警告: [options] ソース値8は廃止されていて、今後のリリースで削除される予定です
警告: [options] ターゲット値8は廃止されていて、今後のリリースで削除される予定です
警告: [options] 廃止されたオプションについての警告を表示しないようにするには、-Xlint:オプションを使用します。
App.java:5: エラー: シンボルを見つけられません
        System.out.println(List.of(1, 2, 3));
                               ^
  シンボル:   メソッド of(int,int,int)
  場所: インタフェース List
エラー1個
警告3個

今回はしっかりコンパイルエラーになりました。とてもすっきりしますね。rt.jarを指定しなくてもOKですし。

とはいえ、実際に過去のバージョン向けにコンパイルできていないと意味がありません。こちらも確認しておきましょう。

ソースコードを以下のように変更。

App.java

import java.util.ArrayList;
import java.util.List;

public class App {
    public static void main(String... args) {
        //System.out.println(List.of(1, 2, 3));
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list);
    }
}

まずはなにも考えずにコンパイル。

$ javac App.java

Java 8で実行。

$ /usr/lib/jvm/java-8-openjdk-amd64/bin/java App
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.UnsupportedClassVersionError: App has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 52.0
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
        at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:621)

クラスファイルのバージョンから、これは実行時エラーになります。

では、--releaseオプションをつけてコンパイル。

$ javac --release 8 App.java
警告: [options] ソース値8は廃止されていて、今後のリリースで削除される予定です
警告: [options] ターゲット値8は廃止されていて、今後のリリースで削除される予定です
警告: [options] 廃止されたオプションについての警告を表示しないようにするには、-Xlint:オプションを使用します。
警告3個

実行。

$ /usr/lib/jvm/java-8-openjdk-amd64/bin/java App
[1, 2, 3]

OKですね。

Maven Compiler Pluginで--releaseオプションを設定する

Javaソースコードを、javacコマンドで直接コンパイルする機会なんてめったにありませんよね。

というわけで、Maven Compiler Pluginでどのように設定するのかも見ておきます。

-sourceおよび-targetオプションの時は、以下のように簡単に指定ができました。

  <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
  </properties>

Apache Maven Compiler Plugin – Setting the -source and -target of the Java Compiler

configurationで設定する場合はこちら。

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.12.1</version>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>

--releaseについても、同じように簡単に指定ができます。

  <properties>
    <maven.compiler.release>8</maven.compiler.release>
  </properties>

Apache Maven Compiler Plugin – Setting the --release of the Java Compiler

configurationで設定する場合はこちらですね。

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.12.1</version>
        <configuration>
          <release>8</release>
        </configuration>
      </plugin>

こちらも確認しておきましょう。

pom.xmlの設定。

    <properties>
        <maven.compiler.release>8</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

ソースコード。

src/main/java/App.java

import java.util.List;

public class App {
    public static void main(String... args) {
        System.out.println(List.of(1, 2, 3));
    }
}

コンパイル。

$ mvn compile

コンパイルエラーになりました。

[INFO] --- compiler:3.11.0:compile (default-compile) @ hello-world ---
[INFO] Changes detected - recompiling the module! :source
[INFO] Compiling 1 source file with javac [debug release 8] to target/classes
[INFO] -------------------------------------------------------------
[WARNING] COMPILATION WARNING :
[INFO] -------------------------------------------------------------
[WARNING] ソース値8は廃止されていて、今後のリリースで削除される予定です
[WARNING] ターゲット値8は廃止されていて、今後のリリースで削除される予定です
[WARNING] 廃止されたオプションについての警告を表示しないようにするには、-Xlint:オプションを使用します。
[INFO] 3 warnings
[INFO] -------------------------------------------------------------
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /path/to/src/main/java/App.java:[5,32] シンボルを見つけられません
  シンボル:   メソッド of(int,int,int)
  場所: インタフェース java.util.List
[INFO] 1 error
[INFO] -------------------------------------------------------------

もしくはこちらをコメントアウトして

    <properties>
        <!-- <maven.compiler.release>8</maven.compiler.release> -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

Maven Compiler Pluginの設定を記述。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version>
                <configuration>
                    <release>8</release>
                </configuration>
            </plugin>
        </plugins>
    </build>

コンパイル結果は同じなので、省略します。

また、実際にJava 8で動作するようにソースコードを修正して、コンパイルした確認結果についても省略します。

おわりに

javacの--releaseオプション(JEP 247 Compile for Older Platform Versions)について、少し調べてみました。

今までIDEの自動生成結果などを頼って以下の記述をしていたことも多かったのですが、releaseも活用した方が安全かつ便利だなと
思いましたね。

  <properties>
    <maven.compiler.source>...</maven.compiler.source>
    <maven.compiler.target>...</maven.compiler.target>
  </properties>

覚えておきましょう。