Java | オブジェクト指向:ポリモーフィズム

Java Java
スポンサーリンク

ポリモーフィズムとは

ポリモーフィズムは「同じ“呼び方”で、実体に応じて振る舞いが切り替わる」仕組みです。呼び出し側は共通の型(親クラスやインターフェース)だけを意識して使い、実際に入っているオブジェクトの種類に応じて適切なメソッド実装が選ばれます。分岐(if/else)をなくし、拡張時にも呼び出し側を変えなくて済むため、コードが読みやすく、保守しやすくなります。


サブタイプ(継承/implements)によるポリモーフィズム(重要)

実体の型でメソッドが選ばれる

参照の型が親でも、実体(new された具体クラス)が子なら、子のオーバーライドが呼ばれます。これが「動的ディスパッチ」です。

abstract class Shape { abstract double area(); }

final class Rect extends Shape {
    private final double w, h;
    Rect(double w, double h){ this.w = w; this.h = h; }
    @Override double area() { return w * h; }
}

final class Circle extends Shape {
    private final double r;
    Circle(double r){ this.r = r; }
    @Override double area() { return Math.PI * r * r; }
}

Shape s = new Rect(3, 4);
System.out.println(s.area());  // 12.0(Rectの実装)

s = new Circle(2);
System.out.println(s.area());  // 12.566...(Circleの実装)
Java

呼び出し側をシンプルにする

共通型のコレクションに入れて、一括処理できます。新しい図形を追加しても、呼び出し側は変更不要です。

double sumArea(java.util.List<Shape> shapes) {
    return shapes.stream().mapToDouble(Shape::area).sum(); // 分岐なし
}
Java

インターフェースによるポリモーフィズム(重要な設計の軸)

役割の契約で差し替え可能にする

implements で契約(インターフェース)を満たせば、実体の差し替えは自由です。テストや本番の切り替えも簡単です。

interface Formatter { String format(String raw); }

final class UpperFormatter implements Formatter {
    @Override public String format(String raw) { return raw == null ? "" : raw.toUpperCase(); }
}
final class SnakeFormatter implements Formatter {
    @Override public String format(String raw) { return raw == null ? "" : raw.trim().replaceAll("\\s+", "_"); }
}

String render(Formatter f, String s) { return f.format(s); } // 実体に応じて切り替わる
Java

ストラテジパターンで分岐をなくす

「やり方の違い」をインターフェースで表し、呼び出し側は戦略を注入するだけにします。追加・変更が呼び出し側に波及しません。

final class NameService {
    private final Formatter formatter;
    NameService(Formatter f) { this.formatter = f; }
    String format(String s) { return formatter.format(s); }
}
Java

オーバーライド、オーバーロードとの違い(重要な深掘り)

オーバーライドがポリモーフィズムの中心

「同じシグネチャ(メソッド名・引数型・並び)」を子が再定義するのがオーバーライドで、これにより実体に応じた切り替えが起きます。@Override を付ける習慣で、意図しないズレを防げます。

class Greeter { String hi(String name){ return "Hi " + name; } }

class FriendlyGreeter extends Greeter {
    @Override String hi(String name){ return "Hello " + name + "!"; } // これが切り替えの核
}
Java

オーバーロードは「同名・別引数」で別物

同じ名前でも引数が違うと別メソッドになり、ポリモーフィズム(動的切替)には関与しません。混同しないように、上書き意図なら必ず @Override を。

class Greeter {
    String hi(String name){ return "Hi " + name; }
    String hi(){ return "Hi"; } // オーバーロード(切り替えの対象外)
}
Java

現場で効く使いどころと型(重要)

テンプレートメソッド+ポリモーフィズム

親が流れを固定し、差し替え点だけ抽象にすると、安全に拡張できます。最小の差分だけを子が実装します。

abstract class Importer {
    public final void run(String path) {
        var raw = load(path);
        var normalized = normalize(raw);
        save(normalized);
    }
    protected abstract String load(String path);           // 差し替え点
    protected String normalize(String s){ return s == null ? "" : s.trim(); } // 共有
    protected abstract void save(String data);             // 差し替え点
}
Java

インターフェースで役割を分ける

「保存する」「整形する」「検証する」など、役割をインターフェースに分けると、必要な契約だけを渡せます。多重実装や委譲との相性が良く、配線が柔らかく保てます。


つまずきやすいポイントと回避(重要な深掘り)

is-a の誤用で契約破綻(LSP違反)

「X は Y の一種」と言えないのに継承すると、親型として扱ったときに挙動が壊れます。拡張はインターフェース+委譲に切り替えるのが安全です。

static、フィールドは動的切替の対象外

動的ディスパッチが効くのはインスタンスメソッドだけ。static メソッドやフィールド参照は「見えている型」に固定されます。切り替えたい処理はインスタンスメソッドに。

コンストラクタで仮想呼び出しをしない

初期化中にオーバーライド可能メソッドを呼ぶと、下位クラスの未初期化フィールドへ触れて事故ります。流れは final メソッドで固定、初期化は「親→子」の順序を厳守。


例題でポリモーフィズムを体感する

例 1: 通知の実装を差し替えても呼び出し側は不変

abstract class Notifier {
    public final void sendAll(java.util.List<String> targets) {
        for (var t: targets) sendTo(t);           // 差し替え点に委ねる
    }
    protected abstract void sendTo(String target);
}

final class EmailNotifier extends Notifier {
    @Override protected void sendTo(String target){ System.out.println("Email -> " + target); }
}

final class SmsNotifier extends Notifier {
    @Override protected void sendTo(String target){ System.out.println("SMS -> " + target); }
}

// 呼び出し側は Notifier 型だけでOK
Notifier n = new EmailNotifier();
n.sendAll(java.util.List.of("taro@example.com"));
Java

例 2: ストラテジの入れ替えで機能拡張

interface Storage { void put(String key, String value); }

final class MemoryStorage implements Storage {
    private final java.util.Map<String,String> map = new java.util.HashMap<>();
    @Override public void put(String key, String value){ map.put(key, value); }
}

final class FileStorage implements Storage {
    @Override public void put(String key, String value){ System.out.println("write " + key + "=" + value); }
}

void saveAll(Storage st) {                          // Storage 契約に依存
    st.put("a","1"); st.put("b","2");
}
Java

仕上げのアドバイス(重要部分のまとめ)

ポリモーフィズムは「共通の呼び方で、実体に応じて振る舞いが切り替わる」仕組みです。インスタンスメソッドのオーバーライドとインターフェースの契約が核で、呼び出し側の分岐を消して拡張に強くします。is-a が成り立つ場面で継承、役割の合成はインターフェース、骨格はテンプレートメソッドで固定。static/フィールドは切替対象外、初期化中の仮想呼び出しは避ける——この線引きを守れば、読みやすく壊れにくい設計が自然に育ちます。

タイトルとURLをコピーしました