JDK 9のjavac "--release"オプションについて
はじめに
2月1日開催の Java読書会「The Java Module Sysmem」を読む会(第4回)において、コラム枠にJDK 9で導入されたjavacコマンドのオプション--releaseの記載がありました。 --releaseオプションは、開発(コンパイル)に使用するJDKのバージョンよりも古いJava SEバージョンで実行可能なクラスファイルをコンパイルするために指定します。そして、JDK 8までの-source、-targetおよび-bootclasspathオプションを置き換えるものです。
JDK 8までの指定
開発(コンパイル)に使用するJDKバージョンより古いバージョンのJavaで実行可能なバイトコード(クラスファイル)を生成するために、javacには以前から次のオプションが用意されています。
- -source
- -target
コンパイルする際に適用するJava言語仕様のバージョンとコンパイルしたクラスファイルを実行可能なバージョンを指定することができます。例えば、JDK 11を開発環境に使っていて、Java SE 8で実行可能なクラスファイルを生成したい場合などに使用します。
ただし、この2つのオプションだけでは、-targetで指定したバージョンより新しいバージョンで追加されたAPIを使ったソースコードをコンパイルしてもエラー、警告等は出ません。ですから、Java SE 8より後に追加されたAPIを呼び出すコードを-source 8 -target 8とオプションを指定してJDK 11のjavacコマンドでコンパイルしてもエラーにはならず、生成されたクラスファイルをJava SE 8で実行するとAPIが存在しないため実行時エラーとなってしまいます。
D:\work> javac -source 8 -target 8 Hello.java 警告:[options] ブートストラップ・クラスパスが-source 8と一緒に設定されていません 警告1個 D:\work> dir /b Hello.class Hello.java D:\work>
後で述べる-bootclasspathが指定されていないと警告が出ますが、コンパイルは通ってクラスファイルは生成されています。
- JDK 8で実行するとエラー
D:\work> java Hello Exception in thread "main" java.lang.NoSuchMethodError: java.lang.Runtime.version()Ljava/lang/Runtime$Version; at Hello.main(Hello.java:3)
そこで、新しいバージョンのJDKを使用して-sourceオプションと-targetオプションを指定し、それより古いバージョンのJava SEで実行できるクラスファイルを生成するには、実行時エラーとなる新しいAPIの使用をコンパイル時にチェックできるよう-bootclasspathオプションを追加します。-bootclasspathには、実行したいバージョンのJDKに含まれるAPIのクラスファイル群(rt.jar)を指定します。
D:\work> javac -source 8 -target 8 -bootclasspath "C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar" ShowRuntimeVersion.java:3: エラー: シンボルを見つけられません System.out.println("Runtime Version = " + Runtime.version()); ^ シンボル: メソッド version() 場所: クラス Runtime エラー1個
-bootclasspathで実行したいバージョンのJDKのAPI(rt.jar)を指定することにより、そのrt.jarに含まれないAPIを使用したソースコードはコンパイルエラーとなります。
-source、-target、-bootclasspath の問題点
- 指定するオプションが3つもあり、それぞれ整合した指定を記述しなくてはならない
- 開発に使用するJDKバージョンの他に実行時のJDKバージョンをインストールしなくてはならない
- -bootclasspathの指定で絶対パスが登場するので扱いが煩雑に(開発環境に制約)
JDK 9からの--releaseオプション(JEP 247)
JDK 9からは、新たな--releaseオプションが追加されました。
これは、従来の-source、-target、および-bootclasspathの3つのオプションに替わり、一括で指定できるようにしたとともに、開発に使用するJDKバージョンだけあればよく、実行したいバージョンのJDKはインストール不要(したがって絶対パス指定も不要)という優れもののオプションです。
D:\work> javac --release 8 Hello.java ShowRuntimeVersion.java:3: エラー: シンボルを見つけられません System.out.println("Runtime Version = " + Runtime.version()); ^ シンボル: メソッド version() 場所: クラス Runtime エラー1個 D:\work>
なぜ指定したバージョンのJavaにはないAPIがエラーとできるのか
--releaseオプションの仕様を定義したJEP 247によると、ct.symファイルに情報があるとのことです。
JDK 11の場合次の場所にct.symがありました。
<JDK 11インストール基点ディレクトリ> +-- bin +-- include +-- jre +-- lib | +-- ct. sym
ct.symはZIP形式アーカイブなので中を見ると次の様にフォルダが並んでいます。
META-INF 6 678 7 76 78 8 87 876 8769 8769A 879 879A 89 89A 9 9-modules 9A A A-modules A789 A9 B
このフォルダ名は、JDKのバージョンを示しているようです。Aは10、Bは11に対応していると思われます。いくつかフォルダの下を探ってみます。
- 6 フォルダの下のファイル構成(一部のみ展開)
6 +-- org +-- javax +-- java | +-- util | +-- sql | +-- security | +-- nio | | +-- channels | | | +-- spi | | | +-- SocketChannel.sig | | | : | | +-- CharBuffer.sig | +-- net | +-- lang | +-- io | +-- beans | +-- awt | +-- applet +--com
全てではないですが、パッケージ名に対応したフォルダとクラス名に拡張子.sigを付けたファイルがツリー構造で存在しています。この拡張子.sigのファイルには、クラス名とメソッドシグニチャが含まれています。javacは、このct.symファイルを参照してコンパイルをしています。
- 9-modulesフォルダの下のファイル構成(一部のみ展開)
9-modules +-- java.activation +-- java.base | +-- module-info.sig +-- java.compiler +-- java.corba +-- java.datatransfer :
こちらには、モジュール定義が格納されています。
- 9フォルダの下のファイル構成(一部のみ展開)
9 +-- com +-- java | +-- awt | +-- io | +-- lang | | +-- Math.sig | | +-- Module.sig | | +-- Runtime$Version.sig | | +-- Runtime.sig | | +-- SecurityManager.sig | | +-- StackWalker$StackFrame.sig | +-- security | +-- time | +-- util +-- javafx +-- javax +--jdk +--sun
JDK 11のct.symには、バージョンとシグニチャの関係が含まれているので、--releaseオプションで指定したバージョンで使用可能なAPIがチェックできるようです。
なお、ct.symの内部構造については仕様が公開されていないので実装依存情報となっている模様です。