CodeGym /コース /JAVA 25 SELF /ポリモーフィズムと抽象の課題

ポリモーフィズムと抽象の課題

JAVA 25 SELF
レベル 23 , レッスン 3
使用可能

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 は「印刷できる」という能力を表すのに適していますが、インターフェースに Stringtitle フィールドを持たせるのは良くありません。
  • キャストの前に 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: 単一責任の原則に反する

抽象があまりに多くの責務を抱えると、保守が難しくなります。複数のインターフェースやクラスに分割する方がよいでしょう。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION