【Java】同じ命令で違う動き?「ポリモーフィズム」をRPGの例えで攻略しよう
はじめに
みなさんどうも、おげんです。
前回は、クラスの機能を効率よく引き継ぐ「継承」についてお話ししました。
同じコードを何度も書かなくて済む「継承」を覚えると、プログラミングがどんどん楽しくなってきますよね。
でも、継承を使いこなしてキャラクター(クラス)を増やしていくと、次にこんな壁にぶつかりませんか?
「戦士も魔法使いも武闘家も、全員に一斉に『攻撃しろ!』って命令を出したい。
でも、一人ひとりの正体を確認して、個別に指示を出すのは面倒すぎる…」
そんな悩みを魔法のように解決してくれるのが、オブジェクト指向の集大成とも言える「ポリモーフィズム(多態性)」です。
今回は、「名前は難しそうだけど、実はめちゃくちゃ便利」なポリモーフィズムについて、RPGを例に解説していきます。
- 「ポリモーフィズム」という言葉を聞いただけで、拒絶反応が出てしまう人
- 継承はわかったけど、それをどう実戦で活かせばいいか悩んでいる人
- たくさんのキャラクターやアイテムを、スマートに一括管理したい人
- オブジェクト指向の「一番カッコいい使い方」を知っておきたい人
ポリモーフィズムって何?
「ポリモーフィズム(多態性)」を一言でいうと、「相手が誰であっても、同じ命令を出せば、それぞれが適切な動きをしてくれる仕組み」のことです。
これだけだと少し抽象的ですよね。RPGの戦闘シーンをイメージしてみてください。
プレイヤーであるあなたは、パーティー全員に「攻撃!」というコマンドを出します。
このとき、あなたは一人ひとりの細かい動きを指示する必要はありません。
- 戦士に「攻撃!」と言えば、剣で斬りつける
- 魔法使いに「攻撃!」と言えば、火の玉を放つ
- 武闘家に「攻撃!」と言えば、拳で殴る
このように、「攻撃しろ!」という命令(メソッド呼び出し)は一つなのに、受け取る相手によって振る舞いが変わる。
これがポリモーフィズムの本質です。
なぜこれが「スマート」なのか?
もしポリモーフィズムがなかったら、プログラムはこんなに大変になります。
「もし相手が戦士なら剣のメソッドを呼んで、もし魔法使いなら魔法のメソッドを呼んで、もし武闘家なら……」と、相手の正体をいちいち確認(条件分岐)しなければなりません。
ポリモーフィズムを使えば、「相手が誰であれ、とにかくCharacterなんだからattack()は持ってるよね!いけ!」と、大雑把(スマート)に指示が出せるようになるんです。
Javaでの書き方:親の型で子を扱う
ポリモーフィズムをプログラムで実現する最大のポイントは、「左側(型)は親クラス、右側(インスタンス)は子クラス」という書き方にあります。
ポリモーフィズムを理解するために、まずは前回の「継承」で作ったクラスをおさらいしておきましょう。
// 親クラス:共通の機能を持つ
public class Character {
String name;
void attack() {
System.out.println(name + "の攻撃!");
}
}
// 子クラス:親を引き継ぎつつ、自分の個性を出す(オーバーライド)
public class Wizard extends Character {
@Override
void attack() {
System.out.println(name + "は魔法を放った!");
}
}この「親」と「子」の関係があるとき、Javaではちょっと不思議なインスタンス化ができるようになります。
不思議な代入の形
通常、インスタンスを作るときは Warrior w = new Warrior(); と書きますが、ポリモーフィズムではこう書くことができます。
// 親(Character)の型に、子(Wizard)を代入する
Character hero = new Wizard();
hero.name = "おげん";
hero.attack(); // 実行結果:「おげんは魔法を放った!」これ、最初に見ると「えっ、型が違うのにいいの?」とびっくりしますよね。
でも、前回の記事で学んだ「is-a関係(戦士はキャラクターの一種である)」が成り立っているからこそ、Javaはこの書き方を許してくれます。
どういう意味があるの?
この書き方のイメージは、「中身は『戦士』なんだけど、とりあえず『キャラクター』として扱うね!」という予約のようなものです。
Character hero1 = new Warrior(); // 中身は戦士
Character hero2 = new Wizard(); // 中身は魔法使い
hero1.attack(); // 実行結果:「おげんの攻撃!」(戦士の動き)
hero2.attack(); // 実行結果:「魔法使いおげんは魔法を放った!」(魔法使いの動き)どちらも変数(箱)の型は Character なので、プログラム側は hero1 も hero2 も、「どっちもCharacterなんだから、とにかくattack()という命令は送れるはずだ」と確信を持って指示が出せます。
そして実際に動かしてみると、Javaが「おっと、この箱の中身は実際には魔法使いだったね」と判断して、上書き(オーバーライド)された適切なメソッドを呼び出してくれるんです。
リスト(List)で真価を発揮する
「親の型で子を扱う」という不思議な書き方。
これの何がそんなに凄いのかというと、「違う種類のクラスを、一つのリストにまとめられる」という点にあります。
RPGのパーティー編成をイメージしてみましょう。
もしポリモーフィズムを使わなかったら…
戦士は Warrior 型、魔法使いは Wizard 型…と別々の型で管理しなければなりません。
// 別々のリストを作らないといけない
List<Warrior> warriors = new ArrayList<>();
List<Wizard> wizards = new ArrayList<>();
// 全員に攻撃指示を出すのも二度手間
for (Warrior w : warriors) { w.attack(); }
for (Wizard wiz : wizards) { wiz.attack(); }これでは、新しい職業(武闘家や盗賊)が増えるたびに、リストもループ処理もどんどん増えていってしまいます。
ポリモーフィズムを使った場合!
「みんな違ってみんな『Character』だよね」という考え方を使えば、一つのリストに全員入れることができます。
// 親の型(Character)のリストに、バラバラな子を入れる!
List<Character> party = new ArrayList<>();
party.add(new Warrior()); // 戦士を追加
party.add(new Wizard()); // 魔法使いを追加
// たった一つのループで、全員に一括指示!
for (Character c : party) {
c.attack();
// 中身が戦士なら剣で、魔法使いなら魔法で勝手に攻撃してくれる!
}この書き方の凄いところは、指示を出す側(メイン処理)が「相手の正体が誰か」を1ミリも気にしなくていいところです。
「君たちが誰であれ、とにかく Character なんだから attack() はできるよね。
さあやって!」と丸投げするだけで、各キャラクターが自分の個性を発揮して動いてくれます。
こうした「修正に強く、スッキリしたコード」が書けると、周りのエンジニアからも「おっ、わかってるね!」と思われるはずですよ!
まとめ:ポリモーフィズムで柔軟なコードへ
今回は、オブジェクト指向の集大成ともいえる「ポリモーフィズム」についてお話ししました。
名前こそ難しそうですが、その正体は「相手が誰であれ、同じ命令で動かせる」という、エンジニアにとって最高に楽ができる仕組みでした。
ポイントを振り返ると:
- ポリモーフィズム: 同じ命令(メソッド)を送っても、相手によって動きが変わること。
- Javaの書き方:
Character c = new Warrior();のように、「親の型に子を入れる」のが基本。 - 最大のメリット: 相手の正体をいちいち確認しなくていいから、リストなどで一括管理できてコードがスッキリする!
最初は「わざわざ親の型に入れる必要ある?」と思うかもしれません。
でも、開発が進んでキャラクターや機能が10個、20個と増えたとき、このポリモーフィズムの本当のありがたさが身に染みてわかるはずです。
「カプセル化」で守り、「継承」で引き継ぎ、「ポリモーフィズム」で自在に操る。
これで、オブジェクト指向の基礎的な武器はすべて揃いました!
これらの武器を少しずつ使いこなして、スマートなエンジニアを目指していきましょう!
今回も最後まで読んでいただき、ありがとうございました!
