多態性おとしあな
今日もJavaネタ。
オブジェクト指向言語の「オーバーロード」「オーバーライド」「ポリモーフィズム」の理解があいまいだとはまりやすい落とし穴についてつらつら書きたいと思います。
まずは簡単なサンプルソースをご紹介しましょう。
Main.java
// ふつうのモンスター class Monster { }
// モンスターを継承して生まれたすげーつえーモンスター class MonsterEx extends Monster { }
// ゆうしゃ(モンスターをこうげきできる) class Hero { public void attack(Monster m) { System.out.println("ゆうしゃ は モンスター に こうげきした!"); } public void attack(MonsterEx m) { System.out.println("ゆうしゃ は すごくつよい モンスター に こうげきした!"); } }
// ふつうのメインクラス class Main { public static void main(String[] args) { // むかしむかし、あるところに、ゆうしゃがいました。 Hero h = new Hero(); // モンスター が あらわれた!(しかもよく見たら強い方だ!) Monster m = new MonsterEx(); // ゆうしゃ の こうげき! h.attack(m); } }
これを実行すると、どうなるでしょうか。
Javaにはオーバーなんとかとか多態性とかいろいろあるから「ゆうしゃ は すごくつよい モンスター に こうげきした!」と表示されるだろうと考えたそこのキミは大間違いです本当にありがとうございました。
実際には、直感に反した結果になります。
実行結果
ゆうしゃ は モンスター に こうげきした!
つよいモンスターではなく、ふつうのモンスターに攻撃したことにされています。なぜでしょう。
ポイントは以下の2つ。
- オーバーロード(多重定義)されたメソッドのどれを実行するかは、コンパイル時に決まる
- どのメソッドが選ばれるかは、引数となる変数を宣言したときの型によって決まる
つまり、一旦
Monster m;
と書いてしまったら、その後 m に Monster オブジェクトを代入しようと MonsterEx オブジェクトを代入しようと、ゆうしゃが m を攻撃したときに呼ばれるのは public void attack(Monster m) の方だけ。
これ、けっこう深刻なバグの元だったりします。
ダブルディスパッチ
そこで発想を逆転させ、攻撃を受ける側(Monster)に「攻撃を受けるメソッド」を作ります。そして、このメソッドを通じて間接的に attack メソッドを呼ぶことにします。こういうのをダブルディスパッチと呼ぶそうです。かっこいいですね。
// ふつうのモンスター class Monster { public void receive(Hero h) { // 勇者に自分を攻撃してもらう h.attack(this); } }
// モンスターを継承して生まれたすげーつえーモンスター class MonsterEx extends Monster { public void receive(Hero h) { // 勇者に自分を攻撃してもらう h.attack(this); } }
Hero クラスは変更なし。
// ふつうのメインクラス class Main { public static void main(String[] args) { // むかしむかし、あるところに、ゆうしゃがいました。 Hero h = new Hero(); // つえーモンスターが あらわれた Monster m = new MonsterEx(); // つえーモンスターは ゆうしゃの こうげきを うけた! m.receive(h); } }
いっけん回りくどい方法ですが、こうすると呼び出すメソッドは実行時に動的に決定されるようになり、意図した結果になります。
実行結果
ゆうしゃ は すごくつよい モンスター に こうげきした!
ちなみに応用例として、今回の Hero クラスを関数オブジェクトにすることで、元の Monster クラスに一切手を触れず、さらに継承という手段も使わず、Monster クラスの機能を拡張することもできます(多少の制約はありますが)。
ねむいので寝る。