【Java】JRE・コンパイラ・クラスファイルのバージョン

Java

バージョンの問題

Javaの開発を行う上で、Javaのバージョンを気にすることは重要です。これは単にバージョンによって使える構文が異なる、といった文法上の違いだけではなく、正しくコンパイルを行ってJavaを実行可能な状態にし、そこからプログラムを正常に実行するためにも、Javaのバージョンを意識しておくことは重要だということです。

個人で開発を行う際は、あまり深く考えずとも単に最新の物をダウンロードしてローカル環境を構築すれば、その時点で最新のバージョンに統一されるためあまり問題はないかもしれません。しかし特にチームで開発を行う場合、各メンバが個人の異なる環境で、異なるタイミングで開発環境を整えることになるため、予めバージョンを揃えておかないと、後で全員の資材を一つにした時に動かないといった問題が起こりかねません。

自分が開発に参加する際、バージョンを指定されることもあると思いますが、そんな時に起こる次のような問題を解消できるよう、このページでは、JRE・コンパイラ・クラスファイルのバージョンについて整理していきます。

  • なぜバージョンを合わせる必要があるのか、バージョンを揃えないとどうなるのかよく理解していない。
  • どのようにバージョンを確認し、設定すれば良いのか知りたい。
  • 基本的な部分を理解して、バージョンに関するエラーが出ても落ち着いて対応できるようにしたい。
  • バージョンを合わせて設定したつもりが出来ていなかった。(JREの設定は出来ていた一方で、コンパイラの設定が出来ておらず異なるバージョンのクラスファイルが作成されていた等)
  • バージョンを合わせるためにどこを確認すれば良いか、何のバージョンを揃えれば良いのか分からない。
  • エラーメッセージにJavaとは異なるバージョン情報(50.0~60.0台)が出力されて対応方法が分からなくなってしまった。

Javaのバージョン

Javaのバージョンと一言で言っても、いくつか指すものがあります。ここでは以下の3つに分けて整理していきます。

  • JRE(実行環境)のバージョン
    • = java コマンドのバージョン
    • $ java --version で確認する。
  • コンパイラのバージョン
    • = javac コマンドのバージョン
    • $ javac --version で確認する。
  • クラスファイルのバージョン
    • = javac コマンドで作成した .class ファイルのバージョン
    • $ javap -v <classes> で確認する。

多くの場合、単にJavaのバージョンと言えば java コマンドすなわちJREのバージョンを指すことかと思われます。しかし実際に開発を行う上では、正常にJavaを実行するために、JREだけでなく、コンパイラのバージョンを正しく設定し、作成されるクラスファイルのバージョンが適切になるように気を付ける必要があります。

JREについては以下で簡単にまとめています

またこれらに加え、便宜的に、特定のJavaのバージョンの構文が記述された .java ファイルを、以降「ソースファイルのバージョン」として説明していきます。例えば、Java15で追加された Text Blocks が記述されている .java ファイルを、“Java15のソースファイル” と呼称するといった具合です。

その上でここまでに挙げた、それぞれがバージョンを持つ各要素は、以下のように関係し合っています。

  • コンパイル
    • コンパイラと同じ又は下のバージョンのソースファイルをコンパイルできる。
    • コンパイラと同じ又は下のバージョンのクラスファイルへコンパイルできる。
  • Javaの実行
    • JRE(実行環境)と同じ又は下のバージョンのクラスファイルを実行できる。

ここからは、上記の二つ(コンパイル → Javaの実行)について順を追って、先に挙げた三つのバージョン(コンパイラ・クラスファイル・JRE)を詳細に確認していきます。

以降それぞれの項で使用する javajavac コマンドの設定方法はこちらに記載しています。

コンパイラのバージョン

まずはソースファイルをコンパイルするところを考えます。この段階でのコンパイラのバージョンに関する注意点は、javac コマンドによるコンパイルでは、コンパイラと同じかそれ以下のバージョンのソースファイルのみをコンパイルすることが可能、ということです。

例えば、Java8, Java11, Java17 の三つのソースファイルに対し、Java11, Java17 の二つのコンパイラを使用する場合、コンパイル可否は次のようになります。

コンパイル可否ソースファイルのバージョンコンパイラのバージョン
可能Java8Java11
可能Java8Java17
可能Java11Java11
可能Java11Java17
× 不可Java17Java11
可能Java17Java17

具体的な実行例を確認してみます。まずは以下のような Main.java を用意します。

import java.util.stream.Stream;
import static java.util.stream.Collectors.joining;

public class Main {
    public static void main(String[] args) {

        // Java8 (Stream)
        String greeting8 = Stream.of("Hello", "World")
                                 .collect(joining(" "));
        System.out.println("Java8 : ".concat(greeting8));

        // Java11 (var)
        var greeting11 = Stream.of("Hello", "World")
                               .collect(joining(" "));
        System.out.println("Java11: ".concat(greeting11));

        // Java17 (String#transform)
        var greeting17 = Stream.of("Hello", "World")
                               .collect(joining(" "));
        System.out.println(greeting17.transform("Java17: "::concat));
    }
}

この main メソッドには、以下の通り Java8, Java11, Java17 の三種類の構文で同じ処理が三回記述されています。

まずこのソースファイルは、Java17のコンパイラを使用してコンパイルすることが可能です。この時、同じバージョンだけでなく、それより古いもの(例えばJava11やJava8の書き方の部分)も問題なくコンパイルできていることが分かります。

# コンパイラ(javacコマンド)のバージョンは 17
$ javac --version
javac 17.0.13

# コンパイル実行
$ javac Main.java

# 正常にJavaを実行できる
$ java Main
Java8 : Hello World
Java11: Hello World
Java17: Hello World

しかしJava11のコンパイラを使用すると、Java8とJava11の部分はコンパイルできますが、Java17の部分でコンパイルエラーが発生してしまいます。

# コンパイラ(javacコマンド)のバージョンは 11
$ javac --version
javac 11.0.25

# Stringのtransformメソッド部分でコンパイルエラー
# Java12で追加されたメソッドのためJava11のコンパイラではコンパイルできない
$ javac Main.java
Main.java:20: エラー: シンボルを見つけられません
        System.out.println(greeting17.transform("Java17: "::concat));
                                     ^
  シンボル:   メソッド transform("Java17: "::concat)
  場所: タイプStringの変数 greeting17
エラー1個

同様にJava8のコンパイラでは、今度はさらに手前のJava11の部分でコンパイルエラーが発生するようになります。

# コンパイラ(javacコマンド)のバージョンは 8
$ javac -version
javac 1.8.0_431

# 型推論のvar部分でコンパイルエラー
# Java10で追加された機能のためJava8のコンパイラではコンパイルできない
$ javac Main.java
Main.java:13: エラー: シンボルを見つけられません
        var greeting11 = Stream.of("Hello", "World")
        ^
  シンボル:   クラス var
  場所: クラス Main
Main.java:18: エラー: シンボルを見つけられません
        var greeting17 = Stream.of("Hello", "World")
        ^
  シンボル:   クラス var
  場所: クラス Main
エラー2個

このように、ソースファイルのバージョン(記載されたJavaの構文)によって、コンパイル可能なコンパイラのバージョンが限定されます。

クラスファイルのバージョン

コンパイラのバージョンは javac--version オプションで確認することができました。しかし見落としがちですが、コンパイラのバージョンとは別に、それによって作成されたクラスファイルにもそれぞれバージョンが存在しています。

クラスファイルのバージョンを確認するには javap コマンドを使用します。これは javac コマンド同様、JDKに同梱されているツールになります。( 【Java】JDK と JRE について

javap コマンドは逆アセンブラであり、逆コンパイラではありません。そのためクラスファイルから元のソースファイルの情報を得ることはできませんが、今回のようにクラスファイルのバージョンを確認したいという場合には便利です。

クラスファイルのバージョンを確認するには -v オプションを使用します。先程の例で使用したJava17でコンパイルしたクラスファイルを確認すると、次のような出力を得られます。

# コンパイラ(javacコマンド)のバージョンは 17
$ javac --version
javac 17.0.13

# コンパイル実行
$ javac Main.java

# 逆アセンブラでクラスファイルのバージョンを確認
$ javap -v Main
Classfile /C:/Users/aaa/bbb/ccc/Main.class
  Last modified yyyy/mm/dd; size 1610 bytes
  SHA-256 checksum xxxxx
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 61
・・・略・・・
  minor version: 0
  major version: 61

メジャーバージョンとマイナーバージョンが別々に出力されていますが、このクラスファイルのバージョンは 61.0 ということが分かります。一見Javaのバージョンとは異なる数字で分かりづらいですが、以下の対応表から、Java17であることを確認できます。(基本的には「Javaのバージョン + 44」と考えれば良さそうです。)

Java SEReleasedMajorSupported majors
1.0.2May 19964545
1.1February 19974545
1.2December 19984645 .. 46
1.3May 20004745 .. 47
1.4February 20024845 .. 48
5.0September 20044945 .. 49
6December 20065045 .. 50
7July 20115145 .. 51
8March 20145245 .. 52
9September 20175345 .. 53
10March 20185445 .. 54
11September 20185545 .. 55
12March 20195645 .. 56
13September 20195745 .. 57
14March 20205845 .. 58
15September 20205945 .. 59
16March 20216045 .. 60
17September 20216145 .. 61
18March 20226245 .. 62
19September 20226345 .. 63
20March 20236445 .. 64
21September 20236545 .. 65
22March 20246645 .. 66
23September 20246745 .. 67

Table 4.1-A. class file format major versions

以上のことから、デフォルトではコンパイラと同じバージョンのクラスファイルが作成されていることが分かります。

デフォルトは同じバージョンですが、それより下のバージョンであれば、javac によるコンパイル時に --release オプションを使用することで、指定したバージョンのクラスファイルを作成することができます。

例えば --release オプションを使用してJava12を指定すると、次のように対応する 56.0 のクラスファイルを作成することができます。

# コンパイラ(javacコマンド)のバージョンは 17
$ javac --version
javac 17.0.13

# Java12を指定してコンパイル実行
$ javac --release 12 Main.java

# 逆アセンブラでクラスファイルのバージョンを確認
$ javap -v Main
Classfile /C:/Users/aaa/bbb/ccc/Main.clas
  Last modified yyyy/mm/dd; size 1610 bytes
  SHA-256 checksum xxxxx
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 56
・・・略・・・
  minor version: 0
  major version: 56

注意点として、コンパイラのバージョンより下であっても、ソースファイルのバージョンより下のバージョンのクラスファイルは作成できないということが挙げられます。これをしようとするとコンパイルエラーとなってしまいます。

例えば次のように、Java10の構文が使用されたソースファイルは、たとえJava17のコンパイラであっても、Java8のクラスファイル(52.0)へコンパイルすることはできません。

# コンパイラ(javacコマンド)のバージョンは 17
$ javac --version
javac 17.0.13

# Java8を指定してコンパイル実行
$ javac --release 8 Main.java
Main.java:13: 警告:リリース10から、'var'は制限された型名であり、型の宣言での使用、または配列の要素タイプとしての使用はできません
        var greeting11 = Stream.of("Hello", "World")
            ^
Main.java:18: 警告:リリース10から、'var'は制限された型名であり、型の宣言での使用、または配列の要素タイプとしての使用はできません
        var greeting17 = Stream.of("Hello", "World")
            ^
Main.java:13: エラー: シンボルを見つけられません
        var greeting11 = Stream.of("Hello", "World")
        ^
  シンボル:   クラス var
  場所: クラス Main
Main.java:18: エラー: シンボルを見つけられません
        var greeting17 = Stream.of("Hello", "World")
        ^
  シンボル:   クラス var
  場所: クラス Main
エラー2個
警告2個

JREのバージョン

ここまで、コンパイラのバージョンとクラスファイルのバージョンをそれぞれ確認してきましたが、java コマンドによるプログラムの実行は、クラスファイルのバージョンが同じかそれより下であれば可能となります。

例えば次のように、Java17のコンパイラによるコンパイルでは、デフォルトで61.0(Java17)のクラスファイルが作成されるため、同じJava17のJRE(javaコマンド)でJavaプログラムを実行することができます。

# コンパイラ(javacコマンド)のバージョンは 17
$ javac --version
javac 17.0.13

# コンパイル実行
$ javac Main.java

# クラスファイルのバージョンは 61.0(Java17)
$ javap -v Main
Classfile /C:/Users/aaa/bbb/ccc/Main.class
  Last modified yyyy/mm/dd; size 1610 bytes
  SHA-256 checksum xxxxx
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 61
・・・略・・・

# JRE(javaコマンド)のバージョンは 17
$ java --version
java 17.0.13 2024-10-15 LTS
Java(TM) SE Runtime Environment (build 17.0.13+10-LTS-268)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.13+10-LTS-268, mixed mode, sharing)

# プログラム実行
$ java Main
Java8 : Hello World
Java11: Hello World
Java17: Hello World

従って次の例のように、Java23のコンパイラではデフォルトで67.0(Java23)のクラスファイルが作成されるため、Java17のJREを使用してこれを実行しようとするとエラーが発生してしまいます。

# コンパイラ(javacコマンド)のバージョンは 23
$ javac --version
javac 23.0.1

# コンパイル実行
$ javac Main.java

# クラスファイルのバージョンは 67.0(Java23)
$ javap -v Main
Classfile /C:/Users/aaa/bbb/ccc/Main.class
  Last modified yyyy/mm/dd; size 1610 bytes
  SHA-256 checksum xxxxx
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 67
・・・略・・・

# JRE(javaコマンド)のバージョンは 17
$ java --version
java 17.0.13 2024-10-15 LTS
Java(TM) SE Runtime Environment (build 17.0.13+10-LTS-268)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.13+10-LTS-268, mixed mode, sharing)

# プログラム実行エラー
$ java Main
エラー: メイン・クラスMainのロード中にLinkageErrorが発生しました
        java.lang.UnsupportedClassVersionError: Main has been compiled by a more recent version of the Java Runtime (class file version 67.0), this version of the Java Runtime only recognizes class file versions up to 61.0

エラー: メイン・クラスMainのロード中にLinkageErrorが発生しました

java.lang.UnsupportedClassVersionError: Main has been compiled by a more recent version of the Java Runtime (class file version 67.0), this version of the Java Runtime only recognizes class file versions up to 61.0
Mainは、より新しいバージョンのJavaランタイム(クラスファイルバージョン 67.0)でコンパイルされています。このバージョンのJavaランタイムは、クラスファイルバージョン 61.0 までしか認識しません。

気を付ける必要があるのは、コンパイラのバージョンではなく、それによって作成されたクラスファイルのバージョンであるという点です。仮にコンパイラのバージョンがJREより上であっても、コンパイル時にオプションを指定して、JREよりも下のバージョンのクラスファイルを作成していれば、問題なくJavaプログラムを実行することが可能となります。

# コンパイラ(javacコマンド)のバージョンは 23
$ javac --version
javac 23.0.1

# コンパイル実行(Java17を指定)
$ javac --release 17 Main.java

# クラスファイルのバージョンは 61.0(Java17)
$ javap -v Main
Classfile /C:/Users/aaa/bbb/ccc/Main.class
  Last modified yyyy/mm/dd; size 1610 bytes
  SHA-256 checksum xxxxx
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 61
・・・略・・・

# JRE(javaコマンド)のバージョンは 17
$ java --version
java 17.0.13 2024-10-15 LTS
Java(TM) SE Runtime Environment (build 17.0.13+10-LTS-268)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.13+10-LTS-268, mixed mode, sharing)

# プログラム実行
$ java Main
Java8 : Hello World
Java11: Hello World
Java17: Hello World
# コンパイラ(javacコマンド)のバージョンは 23
$ javac --version
javac 23.0.1

# コンパイル実行(Java12を指定)
$ javac --release 12 Main.java

# クラスファイルのバージョンは 56.0(Java12)
$ javap -v Main
Classfile /C:/Users/aaa/bbb/ccc/Main.class
  Last modified yyyy/mm/dd; size 1610 bytes
  SHA-256 checksum xxxxx
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 56
・・・略・・・

# JRE(javaコマンド)のバージョンは 17
$ java --version
java 17.0.13 2024-10-15 LTS
Java(TM) SE Runtime Environment (build 17.0.13+10-LTS-268)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.13+10-LTS-268, mixed mode, sharing)

# プログラム実行
$ java Main
Java8 : Hello World
Java11: Hello World
Java17: Hello World

Eclipseの場合

最後にEclipseを利用している場合のバージョン設定について触れておきます。ここまでの説明では、分かりやすいよう主にコマンドを使用した例を挙げてきました。しかし、いざこの内容を活用してエラー解消を行おうとした時、例えばEclipseで開発を行っていると、どこからバージョンの設定を行えば良いのか分からないとなってしまうことがあります。

このように、起きている問題とその解消方法に目途は立っているものの、コマンドではなくIDEから同じ設定を行うには、どうすれば良いか分からないということが往々にしてあります。基礎的な部分が分かっていることと、IDE特有の使用方法に精通していることは異なり、また違った知識が必要になってくるためです。

EclipseでJavaやコンパイラのバージョン設定を行うためには、それぞれ設定画面から操作を行い、バージョンを変更する必要があります。具体的な手順については以下で整理しています。

Eclipseで使用するJavaとコンパイラの設定について具体的な手順を記載しています。