Java | オブジェクト指向:ダウンキャスト

Java Java
スポンサーリンク

ダウンキャストとは何か

ダウンキャストは「親型(抽象クラスやインターフェース)の参照を、より具体的な子型に“下げて”扱う」ことです。たとえば Shape 型の変数を Rect 型として扱いたいときに使います。アップキャスト(子→親)は安全・自動ですが、ダウンキャスト(親→子)は実体がその子型であることが前提なので危険を伴い、誤ると ClassCastException が発生します。基本方針は「なるべく避ける」。必要な振る舞いは親やインターフェースの契約へ“昇格”させ、ポリモーフィズムで解決するのが筋です。


いつ必要になるのか(重要)

設計が未整備で「親型の契約にないメソッドを呼びたい」場面や、外部ライブラリ都合で「戻り値が親型だが中身は特定の子型」であることが分かっている場面です。たとえば GUI フレームワークでイベントの基底型から、特定のイベント型だけ追加情報を取り出したいときなど。ただし、頻繁にダウンキャストが必要になる設計は、共通契約の不足を示す“警告サイン”です。


基本の書き方と安全な確認方法

instanceof で実体確認してからキャストする

実体が目的の子型かを instanceof で確認し、true のときだけキャストします。Java 16+ なら「パターンマッチング for instanceof」で安全・簡潔に書けます。

abstract class Shape { abstract double area(); }
final class Rect extends Shape {
    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 {
    final double r;
    Circle(double r){ this.r = r; }
    @Override double area(){ return Math.PI * r * r; }
}

void printDetail(Shape s) {
    if (s instanceof Rect r) {                 // Java 16+ の書き方
        System.out.println("Rect w=" + r.w + " h=" + r.h);
    } else if (s instanceof Circle c) {
        System.out.println("Circle r=" + c.r);
    } else {
        System.out.println("Unknown shape");
    }
}
Java

Java 15以前でも、次のように書けます。

void printDetailOld(Shape s) {
    if (s instanceof Rect) {
        Rect r = (Rect) s;                     // 実体確認後にキャスト
        System.out.println("Rect w=" + r.w + " h=" + r.h);
    }
}
Java

例題で理解するダウンキャスト

例 1: イベント処理で特定型の追加情報を使う

abstract class Event { abstract String type(); }

final class ClickEvent extends Event {
    private final int x, y;
    ClickEvent(int x, int y) { this.x = x; this.y = y; }
    @Override String type() { return "click"; }
    int x(){ return x; }
    int y(){ return y; }
}

final class KeyEvent extends Event {
    private final int keyCode;
    KeyEvent(int keyCode) { this.keyCode = keyCode; }
    @Override String type() { return "key"; }
    int code(){ return keyCode; }
}

void handle(Event e) {
    if (e instanceof ClickEvent c) {
        System.out.println("Click at " + c.x() + "," + c.y());
    } else if (e instanceof KeyEvent k) {
        System.out.println("Key code " + k.code());
    } else {
        System.out.println("Unknown " + e.type());
    }
}
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) {       // ダウンキャスト不要。契約に依存
    st.put("a","1"); st.put("b","2");
}
Java

「親型にないメソッドを呼びたい」状況自体を、契約設計の見直しで消すのが王道です。


よくある落とし穴と回避(重要な深掘り)

実体が違うのにキャストして例外

親型の変数が常に特定の子型だと“思い込んで”キャストすると、ClassCastException が起きます。必ず instanceof で確認するか、そもそも必要な処理を共通契約へ昇格させてキャストを不要にします。

ダウンキャストだらけの分岐地獄

分岐が増えるほど保守性が落ち、型追加のたびに呼び出し側へ波及します。テンプレートメソッドやダブルディスパッチ(各型が自分の処理を提供し、呼び出し側は共通の入口だけ呼ぶ)で“型ごとの違い”を内側へ閉じ込めましょう。

フィールド・static は動的切り替えの対象外

ダウンキャストは「参照を具体化して見えるメンバーを増やす」だけです。動的ディスパッチが効くのはインスタンスメソッドで、static やフィールド直接参照は見えている型に固定されます。切り替えたい処理はインスタンスメソッドへ寄せるのが安全です。


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

ダウンキャストは「親型の参照を子型として扱う」テクニックで、実体が一致していないと例外が出るため慎重さが必要です。安全に使うなら instanceof(できればパターンマッチ)で実体確認してから。根本的には、必要な振る舞いを親やインターフェースの契約へ昇格させ、呼び出し側は共通型に揃えてポリモーフィズムで解決するのが最善です。ダウンキャストが増えているなら、設計を見直すサインです。

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