SJ blog
backend
A

信頼度ランク

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(); // なぜエラーか

DogAnimal のサブクラスなので、Animal が持つすべてのメソッド・フィールドを持っている(それ以上も持つ可能性がある)。Animal 型の変数が「speak できる・breathe できる」を期待しているとき、Dog はその要件を必ず満たす。

逆に AnimalDog が追加したメソッド(例:fetch())を持っていないかもしれない。Dog dAnimal インスタンスを入れると、d.fetch() の呼び出しが成り立たない可能性がある。これがコンパイルエラーになる理由だ。

【継承ツリーとメモリの関係】

Animal (小さい契約)
  └── Dog (Animalの契約を満たし、追加能力も持つ)

Animal 型変数 ← Dog インスタンス: OK(Dog は Animal の契約を満たす)
Dog 型変数 ← Animal インスタンス: NG(Animal は Dog の追加契約を満たすか不明)

6. Spring DI と繋がる

Spring でよく書くコード:

@Autowired
private UserRepository repository; // インターフェース型で受け取る

実際に注入されるのは JpaUserRepositoryMockUserRepository などの実装クラスのインスタンスだ。

インターフェース型変数     実際のオブジェクト(ヒープ)
repository ──────────────► JpaUserRepository インスタンス
                           (vtable には JPA の実装が入っている)

repository.findById(1L) を呼ぶとき、vtable を通じて実際の実装(JpaUserRepository.findById)が動く。Spring がテスト時に MockUserRepository に差し替えても、呼び出し元のコードは変えなくていい。これがポリモーフィズムを活用した DI の設計思想だ。


まとめ

現象正体
親型変数から子のメソッドが動くvtable を通じた動的ディスパッチ
フィールドは親型の値になるフィールドアクセスはコンパイル時型で解決
ClassCastExceptionJVM が実際のクラス情報との不一致を検出
instanceof実行時にクラス情報を確認する操作
Spring の DIポリモーフィズム + vtable で実装を差し替え可能に

ポリモーフィズムは「仕様」ではなく、JVM が各オブジェクトに持たせる vtable という仕組みから自然に生まれる動作だ。