1. はじめに
ポリモーフィズム は、オブジェクト指向プログラミングの三大概念の一つ(継承とカプセル化と並ぶ)です。語源としては、ギリシャ語の「poly」(多い)と「morph」(形)に由来します。プログラミングでは、1つのインターフェース — 多くの実装を意味します。
定義
ポリモーフィズム とは、異なるクラスのオブジェクトが同じメッセージ(メソッド呼び出し)に対して異なる反応を示す能力のことです。
たとえば、メソッド makeSound() があるとします。これをどんな動物にも呼び出せますが、猫はニャー、犬はワン、牛はモーと鳴きます。プログラマにとっては単なる animal.makeSound() の呼び出しですが、実際に何が起きるかは、その変数の背後にある具体的なオブジェクトの型に依存します。
身近なアナロジー
家にテレビのリモコンがあり、同じリモコンでスピーカーやプロジェクター、さらにはコーヒーメーカーまで操作できると想像してください。あなたが「オンにする」— turnOn() ボタンを押すと、それぞれの機器がそれぞれのやり方で反応します。重要なのは、どの機器にも「電源」ボタンがあることですが、その実装は異なるということです。
Javaの例
class Animal {
void makeSound() {
System.out.println("何らかの音...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("ワン!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("ニャー!");
}
}
class Cow extends Animal {
@Override
void makeSound() {
System.out.println("モー!");
}
}
では次のようにできます:
Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Cow();
animal1.makeSound(); // ワン!
animal2.makeSound(); // ニャー!
animal3.makeSound(); // モー!
注意:すべての変数は Animal 型ですが、呼び出し結果は実際のオブジェクトの型に依存します。
2. ポリモーフィズムの種類
Java(および多くのOOP言語)では、主に2種類のポリモーフィズムが区別されます。
コンパイル時(静的)ポリモーフィズム — メソッドのオーバーロード (overloading)
同じクラスに、同名だがパラメータが異なる複数のメソッドがある場合です。コンパイラは、与えられた引数に基づいてどのメソッドを呼ぶかを決定します。
例(先取り:詳細は次回の講義で):
class Printer {
void print(int x) {
System.out.println("数値: " + x);
}
void print(String s) {
System.out.println("文字列: " + s);
}
}
実行時(動的)ポリモーフィズム — メソッドのオーバーライド (overriding)
メソッドが基底クラスに定義され、サブクラスでオーバーライドされる場合です。どのメソッドが呼ばれるかは、プログラムの実行時(runtime)に、オブジェクトの実際の型に基づいて決まります。
例 — 上の動物の例を参照してください。
3. なぜポリモーフィズムが必要か?
ポリモーフィズム は、面接用の格好いい言葉ではありません。コードを柔軟で拡張可能にし、保守しやすくするための道具です。
コードの汎用性
基底型のオブジェクトを相手に、実装の詳細を気にせずにコードを書けます。たとえば、動物のリストがあれば、それを走査して各要素に makeSound() を呼ぶだけで、猫か犬かを意識する必要はありません。
Animal[] animals = { new Dog(), new Cat(), new Cow() };
for (Animal animal : animals) {
animal.makeSound(); // 毎回「正しい」メソッドが呼ばれる
}
拡張のしやすさ
明日、上司が「オウムを追加しよう!」と言っても、新しいクラス Parrot extends Animal を書いて配列に加えるだけです。他のコードはそのままで構いません。これは 拡張に対して開き、変更に対して閉じる(SOLID の原則 OCP)ということです。
アーキテクチャの簡素化
具体的な実装を意識せず、抽象(基底クラスやインターフェース)を介して各部分が相互作用する複雑なシステムを構築できます。時間と手間、そしてコーヒーを節約できます。
4. 重要な概念:参照型と実際の型
変数の参照型
Animal animal = new Dog(); と書くと、変数 animal の参照型は Animal になります。つまりコンパイラは「これは Animal だ」と考え、Animal クラスで宣言されているメソッドだけを呼び出せるようにします。
オブジェクトの実際(実体)の型
しかしメモリ上に実際に存在するのは Dog 型のオブジェクトです。makeSound() を呼び出したときにどのメソッドが実行されるかは、これにより決まります。
図解
Animal animal = new Dog();
animal.makeSound(); // Animal.makeSound() ではなく Dog.makeSound() が呼ばれる
重要! 基底型(Animal)の参照を通じては、基底クラスで宣言されていない Dog 固有のメソッドを呼び出すことはできません。
遅延(動的)バインディング
これは実行時に起きる「魔法」です。基底型の参照経由でメソッドを呼ぶと、JVM はオブジェクトの実際の型を見て「正しい」実装を呼び出します。これこそが ポリモーフィズムの働き です。
5. 実用例:アプリケーションでのポリモーフィズム
学習用アプリを発展させていきましょう。簡単な動物園シミュレーターを作るとします。基底クラス Animal とその派生クラスがいくつかあります。すべての動物が「鳴く」ことはできるようにしたいが、動物のタイプごとに個別のコードは毎回書きたくありません。
ステップ1:基底クラスとサブクラス
class Animal {
void makeSound() {
System.out.println("何らかの音...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("ワン!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("ニャー!");
}
}
ステップ2:動物の配列
Animal[] zoo = { new Dog(), new Cat(), new Animal() };
ステップ3:走査とメソッド呼び出し
for (Animal animal : zoo) {
animal.makeSound();
}
実行結果:
ワン!
ニャー!
何らかの音...
ここで注目したいのは、配列の中身が誰かを事前に知らなくても、プログラムが自動的に適切なメソッドを呼び分けてくれる点です。
6. ポリモーフィズムの模式図
. Animal (makeSound)
/ \
Dog Cat
(makeSound) (makeSound)
Animal animal = new Dog();
animal.makeSound(); // --> Dog.makeSound()
Animal animal = new Cat();
animal.makeSound(); // --> Cat.makeSound()
7. さらに例:実務的な課題でのポリモーフィズム
たとえば、会社の従業員管理プログラムを書くとします。基底クラス Employee と、Manager と Developer の2つの派生クラスがあります。全員が働く — work() ことはできますが、そのやり方は異なります。
class Employee {
void work() {
System.out.println("従業員が働いている。");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("マネージャーが会議を開いている。");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("開発者がコードを書いている。");
}
}
次のようにできます:
Employee[] staff = { new Manager(), new Developer(), new Employee() };
for (Employee emp : staff) {
emp.work();
}
結果:
マネージャーが会議を開いている。
開発者がコードを書いている。
従業員が働いている。
8. ポリモーフィズムが機能しない場合
ポリモーフィズムが機能するのは、基底クラスで宣言されたメソッドに対してのみです。サブクラスに固有のメソッドがある場合、基底型の参照からは見えません。
class Dog extends Animal {
void fetchStick() {
System.out.println("犬が棒を持ってくる!");
}
}
Animal animal = new Dog();
// animal.fetchStick(); // コンパイルエラー! そのようなメソッドは Animal 経由では見えない
固有のメソッドを呼ぶには、変数を必要な型にキャストする必要があります:
if (animal instanceof Dog) {
((Dog) animal).fetchStick();
}
ただしこれは別の話。要点は、ポリモーフィズムで利用できるのは基底クラスで宣言されたメソッドだけということです。
9. ポリモーフィズムでよくある誤り
誤り1: 基底型の参照からサブクラスのすべてのメソッドが呼べると期待してしまう。実際には、基底クラスで宣言されているメソッドだけが呼べます。
誤り2: メソッドをオーバーライドするときに @Override アノテーションを付けない。これがないと、誤ったシグネチャで別メソッドを書いてしまい、ポリモーフィズムが機能しなくなる(基底クラスのメソッドがオーバーライドされない)可能性があります。
誤り3: キャストせずにサブクラス固有のメソッドを呼ぼうとする。コンパイラは、それが基底型の参照の背後にどの型があるかを知り得ないため許可しません。
誤り4: オーバーロード (overloading) と オーバーライド (overriding) を混同する。オーバーロードは、同一クラス内で同名・異なるパラメータの複数メソッドを持つこと。オーバーライドは、サブクラスでメソッドの振る舞いを変更することです。
GO TO FULL VERSION