Javaを鍛える|OOP基礎編 #4

ポリモーフィズムの基本と実践

  • Java
  • OOP
  • ポリモーフィズム
  • Javaを鍛える

📋 この記事の目次
  1. ポリモーフィズムは「同じ指示で、相手ごとに適切な答えが返る」ことだと考える
  2. 実践:呼び出し側からinstanceofを追放する
  3. こんな場面で使う/使わない
  4. 挫折ポイントTOP3と対処法
  5. この先、何を学べばいいか
  6. まとめ
  7. 🎓 理解度テスト

前回、BugTicketFeatureTicketをそれぞれ独立したクラスに分けました。ところが、チケット一覧を表示する画面のコードを見ると、まだこんな分岐が残っています。

for (Ticket ticket : tickets) {
    if (ticket instanceof BugTicket) {
        BugTicket bug = (BugTicket) ticket;
        System.out.println("[バグ] " + bug.getTitle());
    } else if (ticket instanceof FeatureTicket) {
        FeatureTicket feature = (FeatureTicket) ticket;
        System.out.println("[要望] " + feature.getTitle());
    }
}

クラスは分かれたのに、呼び出し側にまた「型ごとの分岐」が戻ってきてしまいました。今回はポリモーフィズムで、この分岐を消し去ります。


ポリモーフィズムは「同じ指示で、相手ごとに適切な答えが返る」ことだと考える

上司が全部署に「今月の進捗を1行で報告して」と同じ指示を出したとします。経理部・営業部・開発部、それぞれ答える内容は違いますが、「今月の進捗を1行で報告する」という指示の形式は共通です。上司は「相手が経理部だから聞き方を変えよう」などと考える必要がありません。

ポリモーフィズムとは、まさにこの状態です。呼び出す側は「summary()を呼ぶ」という同じ指示を出すだけで、相手(実際のオブジェクトの型)に応じて適切な答えが返ってきます。


実践:呼び出し側からinstanceofを追放する

Step 1: オーバーライドされたsummary()をそのまま呼ぶ

前々回、BugTicketFeatureTicketはすでにsummary()をそれぞれオーバーライドしていました。呼び出し側を書き換えるだけで、型分岐は不要になります。

for (Ticket ticket : tickets) {
    System.out.println(ticket.summary());
}

ticket変数の宣言上の型はTicketですが、実行時には実際に格納されているオブジェクト(BugTicketのインスタンスかFeatureTicketのインスタンスか)に応じて、正しいsummary()が呼ばれます。これを動的バインディング(実行時多態) と呼びます。新しい種類のチケットを追加しても、このループのコードは一切変更する必要がありません。

Step 2: インターフェースで「できること」を分離する

すべてのチケットが通知機能を必要とするわけではない、という設計判断があるとします。この場合、Ticketの継承階層とは別に、「通知できる」という能力だけを表すインターフェースを用意します。

public interface Notifiable {
    void notifyAssignee();
}

public class BugTicket extends Ticket implements Notifiable {
    @Override
    public void notifyAssignee() {
        System.out.println(getAssignee() + "さんにバグ報告の通知を送信しました");
    }
}

FeatureTicketにはNotifiableを実装させない、という選択もできます。継承(is-a関係)とは別軸で「できること」を組み合わせられるのが、インターフェースの強みです。

for (Ticket ticket : tickets) {
    if (ticket instanceof Notifiable notifiable) {
        notifiable.notifyAssignee();
    }
}

ここでのinstanceofは「型で処理を分岐させる」ためではなく、「通知できる相手かどうかを確認する」ためのものです。前回までのinstanceofとは目的が異なる点に注意してください。

if (ticket instanceof Notifiable notifiable)のように条件式の中で変数notifiableをそのまま宣言できるのは、Java 16以降の「パターンマッチング」という比較的新しい構文です。古いJavaではif (ticket instanceof Notifiable) { Notifiable notifiable = (Notifiable) ticket; ... }のようにキャストを別に書く必要があります。

オーバーロードとの違い

ポリモーフィズムにはもう1つ、オーバーロード(同じメソッド名で引数の型・数を変える)という形もあります。

public class TicketFactory {
    public Ticket create(String title) { /* ... */ }
    public Ticket create(String title, String severity) { /* ... */ }
}

オーバーライドが「実行時に、実際のオブジェクトの型に応じて呼び分けられる」のに対し、オーバーロードは「コンパイル時に、渡された引数の型・数に応じて呼び分けられる」という違いがあります。

🧓 ベテランの現場コラム instanceofを見つけたら「ここはポリモーフィズムで解消できないか」を疑う癖をつけると、設計の腕は着実に上がります。ただし今回のNotifiableのように、「特定の能力を持つかどうかの確認」のためのinstanceofは正当な使い方です。「型ごとに処理を分けるためのinstanceof」と「能力を確認するためのinstanceof」を区別できるようになりましょう。

🧓 ベテランの現場コラム 新しいチケット種別を追加したときにコンパイルエラーが1つも出ないと、逆に不安になることがあります。「本当に全部の分岐を直せているか」を確認したいときは、あえてswitch文で全パターンを網羅させ、default節に警告を出すような設計にしておくと安心感が得られます(言語やバージョンによってはsealed classと組み合わせるとより強力になりますが、まずはこの考え方だけ知っておけば十分です)。


こんな場面で使う/使わない

  • 使う: 型ごとに異なる振る舞いをさせたいが、呼び出し側は型を意識したくない場合
  • 使わない: 各型の振る舞いに共通点がほとんどなく、無理に共通のメソッド名でまとめようとするとかえって分かりにくくなる場合(その場合は無理に多態にせず、型ごとに別々の関数を用意した方が読みやすいこともあります)

挫折ポイントTOP3と対処法

1. instanceofに頼りきってしまう

型分岐が増えてきたら、いったん「オーバーライドで表現し直せないか」を考えましょう。

2. オーバーライドとオーバーロードを混同する

「実行時に決まるか、コンパイル時に決まるか」という違いを意識すると区別しやすくなります。

3. インターフェースの導入が後手に回る

「このチケットは通知が必要か」のような“できること”の違いは、継承階層を作った後からでも追加できますが、最初から意識しておくと設計がすっきりします。


この先、何を学べばいいか

Ticketは共通の型として振る舞えるようになりましたが、実はTicketクラス自身が持つclose()のような処理の中に、まだ「本来は種類ごとに実装を強制したい部分」が曖昧なまま残っています。次回は抽象化で、共通の型(インターフェース・抽象クラス)の設計そのものを見直します。


まとめ

  • ポリモーフィズムは「同じ指示で、相手ごとに適切な答えが返る」仕組み
  • オーバーライド(実行時)とオーバーロード(コンパイル時)は別物
  • インターフェースは、継承(is-a関係)とは別軸で「できること」を組み合わせるのに向く
  • instanceofの型分岐を見つけたら、ポリモーフィズムで解消できないか疑う

🎓 理解度テスト

学んだ内容を確認しましょう。全5問。