信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
ポリモーフィズムはなぜ動くのか ── vtable・動的ディスパッチ・キャストの正体
親型の変数に子のインスタンスを入れてメソッドを呼ぶと、なぜ子のメソッドが動くのか。仮想メソッドテーブル(vtable)・動的ディスパッチ・instanceof・ClassCastException をメモリ構造から解説。
一言結論
ポリモーフィズムは「変数の型ではなく、実際のオブジェクトの型でメソッドを決める」仕組み。その実体は JVM が各オブジェクトに持たせる vtable(仮想メソッドテーブル)への間接参照だ。
Animal a = new Dog(); // 親型の変数に子のインスタンス
a.speak(); // "Woof!" が出力される(Dog のメソッドが動く)
これは Java の基本だが、「なぜ親型の変数から子のメソッドが呼べるのか」を説明できる人は少ない。「ポリモーフィズムはそういうもの」と覚えている状態だ。
実際にはメモリとJVMの仕組みがある。理解すると、ClassCastException の原因・instanceof の必要性・Spring の DI がなぜ親型で受け取るのかが全部繋がる。
1. オブジェクトはヒープ上で「自分の型情報」を持っている
new Dog() を実行したとき、ヒープに作られるオブジェクトには型情報へのポインタが含まれる。
【ヒープ上の Dog インスタンス】
┌─────────────────────────────────────┐
│ Dog オブジェクト │
│ │
│ ├─ クラス情報へのポインタ ──────────┼──► Metaspace の Dog クラス情報
│ │ (JVM 内部で常に保持) │
│ ├─ name: "Pochi" │
│ └─ age: 3 │
└─────────────────────────────────────┘
この「クラス情報へのポインタ」が核心だ。 JVM はメソッドを呼ぶとき、このポインタを辿って「このオブジェクトが実際に何者か」を確認する。
2. vtable(仮想メソッドテーブル)とは何か
Metaspace に格納されるクラス情報には、vtable(仮想メソッドテーブル) が含まれている。vtable は「このクラスのメソッド呼び出しがどの実装を使うか」を記した表だ。
class Animal {
void speak() { System.out.println("..."); }
void breathe() { System.out.println("breathing"); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("Woof!"); }
// breathe() はオーバーライドしない
}
class Cat extends Animal {
@Override
void speak() { System.out.println("Meow!"); }
}
【Metaspace の vtable】
Animal の vtable: Dog の vtable: Cat の vtable:
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ speak → Animal版 │ │ speak → Dog版 │ │ speak → Cat版 │
│ breathe → Animal版 │ │ breathe → Animal版 │ │ breathe → Animal版 │
└────────────────────┘ └────────────────────┘ └────────────────────┘
Dog の vtable では speak が Dog 版を指しているが、breathe はオーバーライドしていないので Animal 版のままだ。
3. 動的ディスパッチ ── 実行時にどのメソッドか決まる
Animal a = new Dog();
a.speak(); // どのメソッドが呼ばれるか?
JVM の処理:
1. 変数 a が指すオブジェクトをヒープで確認
→ Dog インスタンス
2. Dog インスタンスの「クラス情報ポインタ」を辿る
→ Metaspace の Dog クラス情報
3. Dog の vtable を参照
→ speak のエントリ → Dog 版の speak
4. Dog 版の speak を実行
→ "Woof!"
これが**動的ディスパッチ(Dynamic Dispatch)**だ。「変数の型(Animal)ではなく、実際のオブジェクトの型(Dog)で決まる」という動作は、vtable を通じた間接参照によって実現している。
フィールドは動的ディスパッチされない
重要な例外:フィールドへのアクセスはコンパイル時型で決まる。
class Animal { String type = "Animal"; }
class Dog extends Animal { String type = "Dog"; }
Animal a = new Dog();
System.out.println(a.type); // "Animal"(コンパイル時型で決まる)
System.out.println(((Dog)a).type); // "Dog"
試験で頻出の「メソッドは実行時型、フィールドはコンパイル時型」はこの仕組みから来ている。
4. キャストと ClassCastException の正体
Animal a = new Dog();
Dog d = (Dog) a; // OK
Cat c = (Cat) a; // ClassCastException!
なぜ Cat へのキャストで例外が起きるか。
a はヒープ上の Dog オブジェクトを指している:
【スタック】 【ヒープ】
┌──────────┐ ┌──────────────────────────┐
│ a: 0xA100│ ────► │ Dog インスタンス │
└──────────┘ │ クラスポインタ → Dog │
└──────────────────────────┘
(Cat) a を試みる:
→ JVM が 0xA100 のオブジェクトのクラス情報を確認
→ 「Dog は Cat ではない(継承関係もない)」と判断
→ ClassCastException
キャストは「このオブジェクトは実はこの型だ」という宣言だ。JVM が実際のクラス情報と照合し、嘘だと判断したら例外を投げる。コンパイルエラーにならない(コンパイラは変数の型だけを見て、実際のオブジェクトの型は実行時にしかわからない)。
instanceof で事前確認する
Animal a = new Dog();
if (a instanceof Dog) {
Dog d = (Dog) a; // 安全
d.fetch();
}
// Java 16+ のパターンマッチング(より簡潔)
if (a instanceof Dog d) {
d.fetch(); // instanceof と同時にキャスト
}
5. 上位互換と下位互換 ── なぜ親型に子を入れられるか
Animal a = new Dog(); // なぜOKか
Dog d = new Animal(); // なぜエラーか
Dog は Animal のサブクラスなので、Animal が持つすべてのメソッド・フィールドを持っている(それ以上も持つ可能性がある)。Animal 型の変数が「speak できる・breathe できる」を期待しているとき、Dog はその要件を必ず満たす。
逆に Animal は Dog が追加したメソッド(例:fetch())を持っていないかもしれない。Dog d に Animal インスタンスを入れると、d.fetch() の呼び出しが成り立たない可能性がある。これがコンパイルエラーになる理由だ。
【継承ツリーとメモリの関係】
Animal (小さい契約)
└── Dog (Animalの契約を満たし、追加能力も持つ)
Animal 型変数 ← Dog インスタンス: OK(Dog は Animal の契約を満たす)
Dog 型変数 ← Animal インスタンス: NG(Animal は Dog の追加契約を満たすか不明)
6. Spring DI と繋がる
Spring でよく書くコード:
@Autowired
private UserRepository repository; // インターフェース型で受け取る
実際に注入されるのは JpaUserRepository や MockUserRepository などの実装クラスのインスタンスだ。
インターフェース型変数 実際のオブジェクト(ヒープ)
repository ──────────────► JpaUserRepository インスタンス
(vtable には JPA の実装が入っている)
repository.findById(1L) を呼ぶとき、vtable を通じて実際の実装(JpaUserRepository.findById)が動く。Spring がテスト時に MockUserRepository に差し替えても、呼び出し元のコードは変えなくていい。これがポリモーフィズムを活用した DI の設計思想だ。
まとめ
| 現象 | 正体 |
|---|---|
| 親型変数から子のメソッドが動く | vtable を通じた動的ディスパッチ |
| フィールドは親型の値になる | フィールドアクセスはコンパイル時型で解決 |
| ClassCastException | JVM が実際のクラス情報との不一致を検出 |
| instanceof | 実行時にクラス情報を確認する操作 |
| Spring の DI | ポリモーフィズム + vtable で実装を差し替え可能に |
ポリモーフィズムは「仕様」ではなく、JVM が各オブジェクトに持たせる vtable という仕組みから自然に生まれる動作だ。