SJ blog
beginner
S

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

javacとjavaコマンドで何が起きているのか — コンパイルから実行までの旅

javacでコンパイル、javaで実行。この2ステップで何が起きているのかを根本から理解する。バイトコード・JVM・クラスパス・ファイル名規則の理由を初心者向けに解説。

一言結論

javacはソースコードをバイトコードに変換し、javaコマンドはJVMでそのバイトコードを実行する。2段階構造がJavaの移植性を生み、ファイル名=クラス名規則はjavacが効率よく探せるための必然だ。

2つのコマンドから始まる疑問

Javaを勉強し始めると、こういう手順を教わる:

javac HelloWorld.java   # コンパイル
java HelloWorld         # 実行

なぜ2ステップ必要なのか。javacjavaは何が違うのか。「とりあえずそういうもの」として覚えていると、エラーが出たときに詰まる。仕組みを理解しよう。

javacは「翻訳機」だ

javacJavaコンパイラ(Java Compiler)だ。人間が書いた.javaファイルを、JVMが読める バイトコード.classファイル)に変換する。

HelloWorld.java  →  [javac]  →  HelloWorld.class
(ソースコード)              (バイトコード)
$ javac HelloWorld.java
$ ls
HelloWorld.class  HelloWorld.java

コンパイルが成功すると.classファイルが生成される。文法エラーがあればここで止まる。エラーはなるべく早い段階で検出する のがコンパイラの重要な役割だ。

バイトコードとは何か

バイトコードは「JVM専用の中間コード」だ。

通常の言語では、コンパイルするとOSやCPU専用のネイティブコード(機械語)になる。Windows向けにビルドしたアプリはLinuxでは動かない。

Javaは違う。コンパイル結果のバイトコードは どんなOSにも依存しない

Windows用JVM ─┐
Linux用JVM  ─ ┼─ 同じ HelloWorld.class が動く
Mac用JVM    ─┘

OSごとのJVMが「バイトコードを自分のOS向けに解釈・実行する」役割を担う。これが「Write Once, Run Anywhere(一度書けばどこでも動く)」というJavaの思想の源だ。

バイトコードは人間には読みにくいが、javap -cコマンドで中身を覗ける:

$ javap -c HelloWorld.class
Compiled from "HelloWorld.java"
public class HelloWorld {
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7  // Field java/lang/System.out
       3: ldc           #13 // String Hello, World!
       5: invokevirtual #15 // Method java/io/PrintStream.println
       8: return
}

これがバイトコードだ。JVMがこれを解釈して実行する。

javaコマンドはJVMを起動する

java HelloWorldJVM(Java Virtual Machine) を起動して、バイトコードを実行するコマンドだ。

java HelloWorld
# = "HelloWorldというクラスのmainメソッドを実行せよ"

javaコマンドに渡すのはファイル名ではなくクラス名 だ。.class.javaも書かない。JVMは指定されたクラス名のバイトコードを探して実行する。

 java HelloWorld.class   # 間違い(.classはつけない)
 java HelloWorld         # クラス名を渡す

内部では:

  1. 指定されたクラスの.classファイルをクラスパスから探す
  2. バイトコードをメモリに読み込む
  3. public static void main(String[] args) メソッドを呼び出す
  4. プログラムが実行される

クラスパス:JVMがクラスを探す場所

JVMはどこでクラスを探すのか?それが クラスパス だ。

java -cp . HelloWorld
# -cp . = 「カレントディレクトリをクラスを探す場所に指定する」

-cp(または-classpath--class-path)でクラスを探すディレクトリやJARファイルを指定する。指定しないとデフォルトでカレントディレクトリが使われる。

JARファイルも指定できる:

java -cp .:lib/commons.jar HelloWorld
# カレントディレクトリとlib/commons.jarの両方から探す(Windowsは ; で区切る)

パッケージがある場合:

# com.example.HelloWorldクラスを実行する場合
java -cp . com.example.HelloWorld
# JVMは ./com/example/HelloWorld.class を探す

クラスパスとパッケージ名(ディレクトリ構造)がセットになって、JVMはクラスを特定できる。

なぜファイル名とpublicクラス名が一致しないといけないのか

Javaには有名な規則がある:

public class Foo を定義するなら、ファイル名は Foo.java でなければならない

これを違反するとコンパイルエラーになる:

// Bar.java というファイル名なのに...
public class Foo {  // ← エラー: class Foo is public, should be declared in a file named Foo.java
    public static void main(String[] args) { }
}

なぜこの規則があるのか?

理由は javacがファイルを効率よく探すため だ。

大規模なプロジェクトでは何千ものソースファイルがある。Main.javaの中でFooクラスを使ったとして、javacはそのソースをどこから探すか?

ルールがなければ全ファイルをスキャンするしかない。でも「ファイル名=公開クラス名」という規則があれば:

「Fooクラスが必要」→「Foo.javaを探せばいい」→ 即座に場所が特定できる

何万ファイルあっても検索は一瞬だ。この規則はコンパイラの効率のために存在する。

逆に言えば、publicでないクラスはこの制約を受けない:

// Foo.java
public class Foo { }   // publicクラスはファイル名と一致(必須)
class Helper { }       // publicでないので同じファイルに書いてOK
class Internal { }     // これも同様

ただし1ファイルにpublicクラスは 1つだけ というのが規則だ。

エラーメッセージを読み解く

理解が深まると、エラーメッセージの意味がわかるようになる:

$ javac Bar.java
Bar.java:1: error: class Foo is public, should be declared in a file named Foo.java
public class Foo {
       ^
1 error

Fooはpublicクラスなのに、Foo.javaではなくBar.javaに書かれている」というエラーだ。

$ java HelloWorld
Error: Could not find or load main class HelloWorld

HelloWorldクラスがクラスパスから見つからない」というエラーだ。.classファイルが存在するか、クラスパスの指定が正しいかを確認する。

まとめ

javac      = ソースコード(.java)→ バイトコード(.class)に変換するコンパイラ
java       = JVMを起動してバイトコードを実行するコマンド(クラス名を渡す)
バイトコード = OS非依存の中間コード(これがJavaの移植性の源)
クラスパス  = JVMがクラスファイルを探すディレクトリ/JARのリスト(-cp で指定)
ファイル名=publicクラス名 = javacが効率よくファイルを探せるための規則

2ステップのビルドプロセスは面倒に見えるが、この構造があるからこそJavaは「どこでも動く」を実現できた。IDEやビルドツールが裏でこれをやってくれているだけで、原理を知っておくとビルドエラーで詰まったときに何が起きているかがわかるようになる。