メインコンテンツまでスキップ

Javaコンパイル

はじめに

Javaコンパイラ(javac)は、人間が読めるソースコード(.java)をJVMが実行できるバイトコード(.class)に変換します。本章では、コンパイルプロセスの各フェーズとバイトコードの構造を解説します。


コンパイルの全体像

┌─────────────────────────────────────────────────────────────────────┐
│ Javaコンパイルの流れ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ソースコード (.java) │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. 字句解析 (Lexical Analysis) │ │
│ │ ソースコードをトークンに分解 │ │
│ │ "public class Foo" → [PUBLIC] [CLASS] [IDENTIFIER:Foo] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 2. 構文解析 (Syntax Analysis / Parsing) │ │
│ │ トークン列から抽象構文木(AST)を構築 │ │
│ │ 文法エラーを検出 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 3. 意味解析 (Semantic Analysis) │ │
│ │ 型チェック、シンボル解決、アノテーション処理 │ │
│ │ 型エラー、未定義変数などを検出 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 4. 中間コード生成・最適化 │ │
│ │ 定数畳み込み、デッドコード除去など │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 5. バイトコード生成 (Code Generation) │ │
│ │ ASTからJVMバイトコードを生成 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ クラスファイル (.class) │
│ │
└─────────────────────────────────────────────────────────────────────┘

各フェーズの詳細

1. 字句解析(Lexical Analysis)

ソースコードを意味のある最小単位(トークン)に分解します。

// ソースコード
public class Hello {
public static void main(String[] args) {
System.out.println("Hello");
}
}
トークン列:
[KEYWORD:public] [KEYWORD:class] [IDENTIFIER:Hello] [LBRACE]
[KEYWORD:public] [KEYWORD:static] [KEYWORD:void] [IDENTIFIER:main]
[LPAREN] [IDENTIFIER:String] [LBRACKET] [RBRACKET] [IDENTIFIER:args] [RPAREN]
[LBRACE]
[IDENTIFIER:System] [DOT] [IDENTIFIER:out] [DOT] [IDENTIFIER:println]
[LPAREN] [STRING:"Hello"] [RPAREN] [SEMICOLON]
[RBRACE]
[RBRACE]

2. 構文解析(Syntax Analysis)

トークン列から抽象構文木(AST: Abstract Syntax Tree)を構築します。

                    CompilationUnit

ClassDeclaration
(name: Hello)

MethodDeclaration
(name: main)

MethodInvocation
/ │ \
receiver method arguments
│ │ │
FieldAccess println StringLiteral
(System.out) ("Hello")

3. 意味解析(Semantic Analysis)

型の整合性やシンボルの解決を行います。

// 型エラーの例
int x = "hello"; // エラー: String を int に代入できない

// 未定義シンボルの例
System.out.println(undefinedVar); // エラー: 変数が定義されていない

// 正しいコード
String message = "hello";
System.out.println(message); // OK: 型が一致
チェック項目
型の互換性int x = "string" → エラー
変数の定義未定義変数の参照 → エラー
メソッドの存在存在しないメソッド呼び出し → エラー
アクセス修飾子private メンバへの外部アクセス → エラー
例外処理checked例外の未処理 → エラー

4. コード生成(Code Generation)

ASTからJVMバイトコードを生成します。

// ソースコード
public int add(int a, int b) {
return a + b;
}
// 生成されるバイトコード
public int add(int, int);
Code:
0: iload_1 // 引数 a をスタックにロード
1: iload_2 // 引数 b をスタックにロード
2: iadd // スタック上の2値を加算
3: ireturn // 結果を返す

バイトコードの構造

クラスファイルフォーマット

┌─────────────────────────────────────────────────────────────────────┐
│ クラスファイル構造 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Magic Number: 0xCAFEBABE │ │
│ │ (Javaクラスファイルの識別子) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Version: minor_version, major_version │ │
│ │ (Java 21 = major version 65) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Constant Pool │ │
│ │ ・クラス名、メソッド名、フィールド名 │ │
│ │ ・文字列リテラル、数値定数 │ │
│ │ ・メソッド参照、フィールド参照 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Access Flags │ │
│ │ (public, final, abstract など) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ This Class / Super Class / Interfaces │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Fields │ │
│ │ (フィールド定義) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Methods │ │
│ │ (メソッド定義 + バイトコード) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Attributes │ │
│ │ (ソースファイル名、アノテーションなど) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

バイトコード命令の種類

カテゴリ命令例説明
ロード/ストアiload, astoreローカル変数とスタック間の転送
算術演算iadd, imul, isub整数演算
型変換i2l, d2i型変換
オブジェクト操作new, getfieldオブジェクト生成、フィールドアクセス
スタック操作dup, pop, swapスタック操作
制御フローif_icmpeq, goto条件分岐、ジャンプ
メソッド呼び出しinvokevirtual, invokestaticメソッド呼び出し
戻り値ireturn, areturnメソッドからの復帰

バイトコードの確認方法

# javap でバイトコードを逆アセンブル
javap -c MyClass.class

# 詳細表示(定数プール含む)
javap -v MyClass.class

# private メンバも表示
javap -p -c MyClass.class

javac オプション

よく使うオプション

# 基本的なコンパイル
javac HelloWorld.java

# 出力先ディレクトリを指定
javac -d out/ src/HelloWorld.java

# クラスパスを指定
javac -cp lib/dependency.jar src/Main.java

# ソースバージョンとターゲットバージョン
javac --source 21 --target 21 Main.java

# すべての警告を表示
javac -Xlint:all Main.java

# 非推奨APIの使用警告
javac -deprecation Main.java

# デバッグ情報を含める
javac -g Main.java

# エンコーディング指定
javac -encoding UTF-8 Main.java

警告オプション(-Xlint)

# 全ての警告を有効化
javac -Xlint:all Main.java

# 特定の警告のみ
javac -Xlint:unchecked,deprecation Main.java

# 特定の警告を無効化
javac -Xlint:all,-serial Main.java
警告タイプ説明
uncheckedジェネリクスの未チェック変換
deprecation非推奨APIの使用
rawtypesraw型の使用
serialSerializable で serialVersionUID がない
finallyfinally ブロックが正常に完了しない
fallthroughswitch の case が fall through

コンパイル時の最適化

javacはいくつかの最適化を行います(ただし、主要な最適化はJIT時に行われます)。

定数畳み込み(Constant Folding)

// ソースコード
int x = 1 + 2 + 3;

// コンパイル後(定数が計算済み)
int x = 6;

文字列連結の最適化

// ソースコード
String s = "Hello" + " " + "World";

// コンパイル後(単一の文字列に)
String s = "Hello World";

デッドコード除去

// ソースコード
if (false) {
System.out.println("never executed");
}

// コンパイル後: この if ブロック全体が除去される

switch の最適化

// 連続した整数の case → tableswitch(配列ルックアップ)
switch (x) {
case 0: ...
case 1: ...
case 2: ...
}

// 離散した値の case → lookupswitch(二分探索)
switch (x) {
case 100: ...
case 500: ...
case 999: ...
}

アノテーションプロセッサ

コンパイル時にアノテーションを処理してコードを生成する仕組みです。

動作の仕組み

┌─────────────────────────────────────────────────────────────────────┐
│ アノテーションプロセッサの処理フロー │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Round 1 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 1. ソースコードをコンパイル │ │
│ │ 2. アノテーションを検出 │ │
│ │ 3. 対応するプロセッサを実行 │ │
│ │ 4. 新しいソースファイルを生成 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ 新しいファイルが生成された場合 │
│ │
│ Round 2 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 生成されたファイルをコンパイル │ │
│ │ 再度アノテーション処理 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ 新しいファイルがなくなるまで繰り返し │
│ │
│ 最終ラウンド │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 全てのクラスファイルを出力 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

代表的なアノテーションプロセッサ

ライブラリ用途
Lombokボイラープレートコード生成(getter/setter等)
MapStructオブジェクトマッピングコード生成
Dagger依存性注入コード生成
AutoValue不変オブジェクト生成
QueryDSL型安全クエリ生成

使用例(Lombok)

// ソースコード
@Data
public class User {
private String name;
private int age;
}

// コンパイル時に生成されるコード
public class User {
private String name;
private int age;

public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public boolean equals(Object o) { ... }
public int hashCode() { ... }
public String toString() { ... }
}

プロセッサの指定

# コマンドラインで指定
javac -processor com.example.MyProcessor Main.java

# META-INF/services による自動検出
# META-INF/services/javax.annotation.processing.Processor に
# プロセッサのFQCNを記載

インクリメンタルコンパイル

問題:全体コンパイルの遅さ

# 全ファイルをコンパイル(大規模プロジェクトでは遅い)
javac src/**/*.java

解決:変更されたファイルのみコンパイル

ビルドツール(Gradle、Maven)はインクリメンタルコンパイルをサポートしています。

┌─────────────────────────────────────────────────────────────────────┐
│ インクリメンタルコンパイルの仕組み │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 変更検出 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ ・ソースファイルのタイムスタンプ │ │
│ │ ・クラスファイルのタイムスタンプ │ │
│ │ ・依存関係グラフ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ コンパイル対象の決定 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 1. 変更されたファイル │ │
│ │ 2. 変更されたファイルに依存するファイル │ │
│ │ 3. 削除されたファイルを参照していたファイル │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

Gradle でのインクリメンタルコンパイル

// build.gradle.kts
tasks.withType<JavaCompile> {
options.isIncremental = true // デフォルトで有効
}

コンパイルエラーの種類

構文エラー(Syntax Error)

// セミコロン忘れ
int x = 10 // error: ';' expected

// 括弧の不一致
if (x > 0 { // error: ')' expected
}

型エラー(Type Error)

// 型の不一致
String s = 123; // error: incompatible types

// メソッドの引数不一致
Math.max("a", "b"); // error: no suitable method found

シンボルエラー(Symbol Error)

// 未定義の変数
System.out.println(undefined); // error: cannot find symbol

// 未定義のクラス
UnknownClass obj = new UnknownClass(); // error: cannot find symbol

アクセスエラー(Access Error)

// private メンバへのアクセス
class Other {
private int secret = 42;
}
class Main {
void test(Other o) {
System.out.println(o.secret); // error: secret has private access
}
}

まとめ

項目ポイント
コンパイルフェーズ字句解析 → 構文解析 → 意味解析 → コード生成
クラスファイルMagic Number (CAFEBABE)、定数プール、バイトコード
javapバイトコードの逆アセンブル・確認ツール
javac オプション-d, -cp, -Xlint, -g など
アノテーションプロセッサコンパイル時コード生成(Lombok等)
インクリメンタルコンパイル変更ファイルのみ再コンパイル

次のステップ