信頼度ランク
| 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ステップ必要なのか。javacとjavaは何が違うのか。「とりあえずそういうもの」として覚えていると、エラーが出たときに詰まる。仕組みを理解しよう。
javacは「翻訳機」だ
javac は Javaコンパイラ(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 HelloWorld は JVM(Java Virtual Machine) を起動して、バイトコードを実行するコマンドだ。
java HelloWorld
# = "HelloWorldというクラスのmainメソッドを実行せよ"
javaコマンドに渡すのはファイル名ではなくクラス名 だ。.classも.javaも書かない。JVMは指定されたクラス名のバイトコードを探して実行する。
❌ java HelloWorld.class # 間違い(.classはつけない)
✅ java HelloWorld # クラス名を渡す
内部では:
- 指定されたクラスの
.classファイルをクラスパスから探す - バイトコードをメモリに読み込む
public static void main(String[] args)メソッドを呼び出す- プログラムが実行される
クラスパス: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やビルドツールが裏でこれをやってくれているだけで、原理を知っておくとビルドエラーで詰まったときに何が起きているかがわかるようになる。