はじめに
Javaはコンパイル言語ですので、Java言語でソースコードを書いた後は必ずコンパイルを行います。Javaのコンパイラは文法チェックとバイトコード(クラスファイル)への変換を行ってくれますが、実は裏でこっそりさまざまな実装も行ってくれています。これはつまりプログラマが書いたコードと、コンパイル後のコードは異なっている場合がある、ということなんです。
Javaの入門書などで学習していると「自動的に作成されます」や「〇〇は省略できます」といった文言をときどき見かけるときがあると思います。これはプログラマが書いてもいいし、書かなくてもいいという暗黙的な約束事を指しています。ただしコンピュータは曖昧な命令では実行できませんので、暗黙的な約束事は本来、きちんを書かなければいけない約束事なはずです。
プログラマが書かなくてもいいのなら、では誰が一体これらの暗黙的な約束事を実装してくれているのでしょうか?
すでにお察しの通り、コンパイラが実装していることになります。暗黙の約束事の実装以外にもコンパイラはさまざまな実装を行っています。
Javaのプログラムを実行するJVM(Java仮想マシン)は、プログラマが書くJava言語のコードを実行するのではなく、コンパイラが出力するクラスファイルに書かれているバイトコードを実行しています。つまりJava言語のソースコードには書かれてないコードを実行している場合もあるということです。コンパイラが出力するバイトコードの中身を見ることでJavaのソースコードをより深く理解することができます。
そこで今回は「バイトコードの真髄」です。
コンパイルした後のバイトコード(クラスファイル)の中身を見るべし
Javaでよく出てくる代表的な暗黙事項には次のようなものがあります。
- デフォルトコンストラクタ
- super()の暗黙的な呼び出し
- インターフェースで宣言されるフィールドやメソッドの修飾子
- return文の省略
これらはすべてプログラマが明示的に書く必要のない暗黙事項です。まずはこれらの暗黙事項についてコンパイラがどのように実装しているのかをいくつか見てみましょう。
デフォルトコンストラクタの例
Javaのソースコード例
public class Sample{
}
コンパイル後のクラスファイルの実装内容(「javap -c Sample」の実行結果)
public class Sample {
public Sample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
}
Javaのソースコード上はSampleクラスには何も宣言がなく空実装になっていますが、コンパイル後のクラスファイルには2行目~6行目に引数なしコンストラクタの宣言が実装されています。これがデフォルトコンストラクタの正体(コンパイラが実装したコード)になります。
続いて「インターフェースで宣言されるフィールドやメソッドの修飾子」を見てみましょう。
インターフェースで宣言されるフィールドやメソッドの修飾子
Javaのソースコード例
public interface Sample{
int num;
void method();
}
コンパイル後のクラスファイルの実装内容(「javap -c Sample」の実行結果)
public interface Sample {
public static final int num;
public abstract void method();
}
Javaのソースコード上はフィールドやメソッドに修飾子は明記していませんが、コンパイル結果をみるとフィールドには「public static final」、メソッドには「public abstract」という修飾子が実装されています。
enumの実装例
次はenum宣言をコンパイルしたときのバイトコードを見てみましょう。enum(列挙型)は定数を使用したいときによく使う構文です。C言語でもenumはおなじみですが、C言語のenumとは全く使い方が異なります。Javaのenumはただの定数ではなく、実はれっきとしたオブジェクトです。その証拠に暗黙的にname()やordinal()、values()といったメソッドを使用することができます。
プログラマが書くソースコードはただの定数宣言だけのコードのように見えますが、バイトコードを見るとオブジェクトであることがよく分かります。
Javaのソースコード例
public enum Direction{
EAST,WEST,SOUTH,NORTH
}
コンパイル後のクラスファイルの実装内容(「javap -c Direction」の実行結果)
public final class Direction extends java.lang.Enum<Direction> {
public static final Direction EAST;
public static final Direction WEST;
public static final Direction SOUTH;
public static final Direction NORTH;
public static Direction[] values();
Code:
0: getstatic #16 // Field $VALUES:[LDirection;
3: invokevirtual #20 // Method "[LDirection;".clone:()Ljava/lang/Object;
6: checkcast #21 // class "[LDirection;"
9: areturn
public static Direction valueOf(java.lang.String);
Code:
0: ldc #1 // class Direction
2: aload_0
3: invokestatic #25 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #1 // class Direction
9: areturn
static {};
Code:
0: new #1 // class Direction
3: dup
4: ldc #35 // String EAST
6: iconst_0
7: invokespecial #36 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #3 // Field EAST:LDirection;
13: new #1 // class Direction
16: dup
17: ldc #37 // String WEST
19: iconst_1
20: invokespecial #36 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #7 // Field WEST:LDirection;
26: new #1 // class Direction
29: dup
30: ldc #38 // String SOUTH
32: iconst_2
33: invokespecial #36 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #10 // Field SOUTH:LDirection;
39: new #1 // class Direction
42: dup
43: ldc #39 // String NORTH
45: iconst_3
46: invokespecial #36 // Method "<init>":(Ljava/lang/String;I)V
49: putstatic #13 // Field NORTH:LDirection;
52: invokestatic #40 // Method $values:()[LDirection;
55: putstatic #16 // Field $VALUES:[LDirection;
58: return
}
少し見にくいですが、1行目の宣言文を見てみましょう。
public final class Direction extends java.lang.Enum<Direction> {
enum宣言は、実はfinalなクラスでEnumクラスを継承していることが分かります。
次にフィールド宣言を見てみましょう。
public static final Direction EAST;
public static final Direction WEST;
public static final Direction SOUTH;
public static final Direction NORTH;
定数として宣言していた部分は実は自分の型で宣言されたフィールドであることが分かります。それらのフィールドは25行目から始まるstaticイニシャライザ内でインスタンス化され、0,1,2,3というint型の値と紐付けられて初期化されていることが分かります。
さらに2つのstaticメソッドが宣言されていることも分かります。
public static Direction[] values();
public static Direction valueOf(java.lang.String);
暗黙的に使えるその他のメソッド、name()、ordinal()、toString()などは宣言されていませんが、継承元のEnumクラスが持っていることも分かりますね。
まとめ
「バイトコードの真髄」に触れていただけましたでしょうか?
Javaの初心者にとってコンパイラは「文法をチェックしてくれる便利なツール」くらいの意識しかないかもしれませんが、プログラマが書く手間を省いてくれたり、効率の良い実装に変換してくれたり、裏で大活躍してくれています。ただし、自動で実装されるコードはプログラマが書いたソースコードには反映されないため、どのような実装になっているかはプログラマが自発的に見に行かないと分からないです。enumの例のようにバイトコードの実装内容を見ると、Java言語での記述の仕方もすごく腹落ちすると思います。Javaの初心者の方はぜひバイトコードの実装内容も確認する癖をつけるようにしましょう。
今回も最後までお読みいただき、ありがとうございました。
番外編~コンパイラの自動実装に対する賛否~
コンパイラによる自動実装はプログラマの手間を省いてくれますが、これはJavaの暗黙の約束事を理解している人にとっては非常に便利な仕組みですが、Javaの初心者にとっては弊害がある仕組みかもしれません。なぜならJava言語に不慣れな初心者が省略されたコードばかりを読んでいるとそれが正しいコードだと勘違いしてしまいかねないからです。初心者の方はJavaに慣れるまでは省略できるところも全て明示的に書くようにしましょう。
※アイキャッチ画像はChatGPTで作成しています