1. ポリモーフィズムとは何か、なぜ必要か
もしポリモーフィズムが Marvel のミュータントの話だと思っているなら残念ながら違います。プログラミングではもっと静かですが、同じくらい魔法のようです。ポリモーフィズムとは、異なる実装を持つオブジェクトが、同じメソッド呼び出しに対して異なる反応を示す能力のことです。
身近な例:
クラス Book とクラス Magazine があり、どちらも抽象クラス LibraryItem を継承しているとします。ライブラリの任意の要素に対してメソッド printInfo() を呼び出せば、必要な情報が出力されるようにしたい――本なら著者とタイトル、雑誌なら号数と日付、という具合です。
コード例:
abstract class LibraryItem {
String title;
LibraryItem(String title) {
this.title = title;
}
abstract void printInfo();
}
class Book extends LibraryItem {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void printInfo() {
System.out.println("本: " + title + ", 著者: " + author);
}
}
class Magazine extends LibraryItem {
int issueNumber;
Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
void printInfo() {
System.out.println("雑誌: " + title + ", 号: " + issueNumber);
}
}
これで、異なる要素を同じ配列に入れて各要素に対して printInfo() を呼び出せます:
LibraryItem[] items = {
new Book("蠅の王", "ウィリアム・ゴールディング"),
new Magazine("科学と生活", 5)
};
for (LibraryItem item : items) {
item.printInfo();
}
// 出力:
// 本: 蠅の王, 著者: ウィリアム・ゴールディング
// 雑誌: 科学と生活, 号: 5
これがポリモーフィズムの動作です!
2. ポリモーフィズムでよくあるミス
基底型に存在しないメソッドを呼び出そうとする
最もよくあるミスのひとつは、派生クラスにしか宣言されていないメソッドを、基底型の参照を通して呼び出そうとすることです。
LibraryItem item = new Book("ハリー・ポッター", "J・ローリング");
// item.getAuthor(); // コンパイルエラー! LibraryItem には getAuthor() メソッドがありません
Java は変数の型(LibraryItem)に見えているものを基にコンパイルし、実際のオブジェクト(Book)では判断しません。したがって、本に特有のメソッドを呼び出す必要があるなら、キャストが必要です:
if (item instanceof Book) {
Book book = (Book) item;
// これで book.getAuthor() を呼び出せます
}
型チェックなしのキャスト
オブジェクトが Book であると「確信」していても、実際にはそうでない場合、実行時に ClassCastException になります。例えば:
LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // ドカン! ClassCastException
正しいやり方 — 必ず型を確認すること:
if (item instanceof Book) {
Book book = (Book) item;
// OK
} else {
System.out.println("これは本ではありません!");
}
ポリモーフィズムの利点を活かせていない
特定の型にベタに結びついたコードを書いてしまい、抽象化を使えばよいのに使わないことがあります。例えば次のように書くと:
Book[] books = ...;
for (Book book : books) {
book.printInfo();
}
これは本にしか機能しません。明日、雑誌や新聞、コミックが増えたらどうしますか? 代わりに LibraryItem[] の配列を使い、基底クラスやインターフェースのメソッドで扱う方がよいでしょう。
3. 抽象: 何のためにあり、どう壊さないか
抽象クラスとインターフェース
抽象とは、重要な点を抽出して詳細を隠す技術です。Java ではそのために抽象クラスとインターフェースがあります。
- 抽象クラス — 直接生成できず、継承してのみ使えるクラス。
- インターフェース — クラスが「何ができるか」を定める契約であり、「どのように行うか」は定めません。
誤り 1: 抽象メソッドのない抽象クラスを作る
抽象クラスに抽象メソッドがひとつもないなら、本当に抽象にすべきか再考しましょう。通常のクラスにした方が簡単かもしれません。
abstract class UselessAbstract {
void sayHello() {
System.out.println("Hello!");
}
}
// 抽象メソッドがないなら通常のクラスにした方がよい
誤り 2: サブクラスで必須メソッドを実装しない
クラスが抽象クラスを継承したりインターフェースを実装する場合、すべての抽象メソッドを実装する義務があります。忘れればコンパイラが教えてくれますが、「形だけ」の実装で何もしないままにすることもあります。これは保守性の観点でよくありません。
class Magazine extends LibraryItem {
Magazine(String title, int issueNumber) {
super(title);
// ...
}
@Override
void printInfo() {
// 空っぽ! ダメ!
}
}
誤り 3: 深すぎたり分かりにくい抽象の階層
クラスが5〜10段も継承していると、把握が非常に難しくなります。より「平らな」階層にして、分かりやすくするのがよいでしょう。
悪い例:
LibraryItem
|
BookItem
|
PrintedBook
|
IllustratedBook
|
ChildrenIllustratedBook
複雑ですよね。2〜3階層にとどめるのがよいでしょう。
4. 実践: 学習アプリでのポリモーフィズムと抽象の適用
あなたの図書館アプリを拡張してみましょう。これまでは本だけでしたが、雑誌を追加し、印刷物の共通インターフェースを実装します。
抽象クラスを宣言:
abstract class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public abstract void printInfo();
}
サブクラスを追加:
class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
@Override
public void printInfo() {
System.out.println("本: " + title + ", 著者: " + author);
}
}
class Magazine extends LibraryItem {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
public void printInfo() {
System.out.println("雑誌: " + title + ", 号: " + issueNumber);
}
}
ポリモーフィズムを使う:
LibraryItem[] items = {
new Book("クリーンコード", "ロバート・マーティン"),
new Magazine("Java World", 3)
};
for (LibraryItem item : items) {
item.printInfo();
}
電子版のためのインターフェースを追加
一部の刊行物はオンラインで読めるとします。次のインターフェースを導入します:
interface ReadableOnline {
void openOnline();
}
class EBook extends Book implements ReadableOnline {
private String url;
public EBook(String title, String author, String url) {
super(title, author);
this.url = url;
}
@Override
public void openOnline() {
System.out.println("電子書籍を次のURLで開きます: " + url);
}
}
これで電子書籍をインターフェース経由で扱えます:
ReadableOnline ebook = new EBook("Java超入門", "バリー・バード", "https://example.com/java");
ebook.openOnline();
5. ポリモーフィズムと抽象で問題を避けるには: ベストプラクティス
- 振る舞いの記述にインターフェースや抽象クラスを使い、状態の保持には使わない。
例えば、インターフェース Printable は「印刷できる」という能力を表すのに適していますが、インターフェースに String の title フィールドを持たせるのは良くありません。 - キャストの前に instanceof で型を確認する。
オブジェクトが複数の型になりうる場合は特に重要です。これで ClassCastException を防げます。 - 「平ら」で分かりやすい階層を目指す。
継承ツリーが単純であるほど、保守や拡張が容易になります。 - 「意味のない」抽象を作らない。
抽象メソッドがなく、継承させる意図もないなら、そのクラスを抽象にすべきではありません。 - メソッドをオーバーライドする際は @Override を常に付ける。
シグネチャの誤りをコンパイラが検出しやすくなります。
6. ポリモーフィズムと抽象での典型的な誤り
エラー 1: 型チェックなしのキャスト
「ショートカット」を狙って型チェックせずにキャストしたくなることがあります。動く場合もありますが、突然のクラッシュを招くことも。必ず instanceof を使いましょう:
if (item instanceof Book) {
Book book = (Book) item;
// ...
}
エラー 2: 基底型の参照でサブクラスのメソッドを呼ぼうとする
LibraryItem item = new Book("Java", "著者");
item.getAuthor(); // コンパイルエラー: LibraryItem にはそのようなメソッドはありません!
解決策 — 型をキャストするか、(論理的に妥当なら)必要なメソッドを基底クラスに追加します。
エラー 3: インターフェースや抽象クラスの不完全な実装
インターフェースの全メソッドを実装し忘れると、コンパイラがビルドを許しません。しかし、何もしない「スタブ」を実装してしまうと、予期しない挙動につながります。
エラー 4: 深すぎる継承階層
継承が3段以上になっている場合、設計を単純化できないか検討しましょう。
エラー 5: 単一責任の原則に反する
抽象があまりに多くの責務を抱えると、保守が難しくなります。複数のインターフェースやクラスに分割する方がよいでしょう。
GO TO FULL VERSION