Java | オブジェクト指向:アップキャスト

Java Java
スポンサーリンク

アップキャストとは何か

アップキャストは「子クラス(具体的な型)の参照を、親クラス(より抽象的な型)やインターフェースの参照として扱う」ことです。型を“上位”に持ち上げるイメージなのでアップ(up)キャストと呼びます。Javaでは互換性がある関係なら自動で行われ、通常キャスト演算子は不要です。目的は、呼び出し側を共通の型に揃えてポリモーフィズムを使えるようにし、分岐(if/else)を減らして拡張に強くすることです。


何が起こるか(実体と参照型の関係を理解する)

実体はそのまま、参照型だけが“抽象化”される

アップキャストは「参照の見方」を変えるだけで、オブジェクトの実体は変わりません。実体に応じたメソッドのオーバーライドは引き続き効きます(動的ディスパッチ)。つまり、親型の変数で受けても、呼ばれる実装は“実体の型”に依存します。

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);           // 実体は Rect、参照型は Shape
System.out.println(s.area());       // Rect の実装が呼ばれる

s = new Circle(2);                  // 実体を差し替え
System.out.println(s.area());       // Circle の実装が呼ばれる
Java

参照型にないメンバーは見えなくなる(アクセスは絞られる)

アップキャストすると、参照型(親やインターフェース)に定義されていないメンバーは“見えません”。ただし、見えているインスタンスメソッドは実体に応じて切り替わります。フィールド参照や static メソッドの呼び出しは見えている型に固定されるため、動的には切り替わりません。


インターフェースへのアップキャスト(実務で最も使う形)

役割の契約にそろえ、差し替えを自由にする

インターフェース型へアップキャストすると、呼び出し側は契約だけに依存できます。実体の差し替え(本番用・テスト用)が容易になり、ポリモーフィズムの恩恵が最大化します。

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+", "_"); }
}

// 子 → インターフェースへのアップキャスト(自動)
Formatter f = new UpperFormatter();
System.out.println(f.format(" Hello "));  // 実体 UpperFormatter が選ばれる

f = new SnakeFormatter();
System.out.println(f.format("a  b"));     // 実体 SnakeFormatter が選ばれる
Java

コレクションでのアップキャスト(拡張に強い設計)

共通型のリストで一括処理

異なる具体型を共通の親型やインターフェースにアップキャストして同じコレクションに入れると、一括処理がシンプルになります。新しい実装を追加しても、呼び出し側は変更不要です。

java.util.List<Shape> shapes = java.util.List.of(
    new Rect(3,4),      // 自動アップキャスト
    new Circle(2)       // 自動アップキャスト
);

double total = shapes.stream().mapToDouble(Shape::area).sum();
System.out.println(total);
Java

ジェネリクスでは「参照型の汎用化」が鍵です。List<Rect> を List<Shape> に直接代入はできないため、最初から List<Shape> に入れるか、必要に応じてストリームなどで詰め替えます。


アップキャストとダウンキャストの違い(重要な線引き)

アップキャストは常に安全、ダウンキャストは実体チェックが必要

  • アップキャストは「子 → 親/インターフェース」。型の互換性が保証されるため、例外は起きません(自動)。
  • ダウンキャストは「親 → 子」。参照が本当にその子の実体を指していないと ClassCastException が出ます。instanceof で実体を確認してからのみ行うべきです。
Shape s = new Rect(3,4);        // アップキャスト(安全)
Rect r = (Rect) s;              // ダウンキャスト(安全:実体が Rect)

Shape t = new Circle(2);
Rect x = (Rect) t;              // 実体が Rect でない → ClassCastException
Java

基本方針は「安易なダウンキャストを避ける」。呼び出し側は共通型に揃え、必要な機能は親やインターフェースの契約へ昇格させるのが筋です。


よくある誤解と回避(深掘り)

「アップキャストすると遅くなる/機能が減る」わけではない

機能は“見えている型のメンバー”に限定されますが、実体のオーバーライドは効き続けます。必要な機能が親に無いなら、契約設計を見直してインターフェースへ切り出すのが正解です。

フィールドや 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 n = new EmailNotifier();             // アップキャスト
n.sendAll(java.util.List.of("taro@example.com"));
Java

Notifier 型に揃えれば、Email/SMS/Push の追加が呼び出し側へ波及しません。

例 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

呼び出し側は Storage 契約だけに依存し、差し替えが容易になります。


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

アップキャストは「子の実体はそのまま、参照型だけを親やインターフェースに抽象化して扱う」技で、ポリモーフィズムを生かすための入口です。安全(自動)で、呼び出し側を共通型に揃えられ、分岐が消えます。見えているメンバーは参照型に制限されるため、必要な機能は契約へ昇格させるのが設計の筋。ダウンキャストは実体チェックが必須で、極力避ける——この線引きを徹底すると、拡張に強く壊れにくいコードになります。

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