Java | オブジェクト指向:動的バインディング

Java Java
スポンサーリンク

動的バインディングとは

動的バインディングは「どのメソッド実装を呼ぶかを、実行時の“実体の型”に基づいて決める仕組み」です。参照の型が親(抽象クラスやインターフェース)でも、実際に入っているオブジェクトが子なら、子のオーバーライドが選ばれます。これにより、呼び出し側は共通の型だけに依存し、分岐を増やさずに拡張できます。Java のポリモーフィズムは、この動的バインディングが核です。


仕組みの核心(実体で決まる、参照型では決まらない)

実体の型に基づくメソッド選択

呼び出し時に JVM が実体のクラスを見て、オーバーライドの最下段(最も具体的)を選びます。参照の型は「見えるメンバーを制限」する役割であり、「どの実装が呼ばれるか」は実体で決まります。

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);     // 参照は Shape、実体は Rect
System.out.println(s.area()); // Rect の実装(実体で決まる)

s = new Circle(2);
System.out.println(s.area()); // Circle の実装
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+", "_"); }
}

Formatter f = new UpperFormatter();
System.out.println(f.format(" Hello "));
f = new SnakeFormatter();
System.out.println(f.format("a  b"));
Java

動的にならないもの(重要な線引き)

フィールド参照と static メソッドは静的バインディング

動的バインディングが効くのは「インスタンスメソッド」だけです。フィールド参照や static メソッド呼び出しは、コンパイル時に“見えている型”で決まります。切り替えたい処理は、インスタンスメソッドに寄せるのが鉄則です。

class Base { static void x(){ System.out.println("Base"); } }
class Sub extends Base { static void x(){ System.out.println("Sub"); } }

Base b = new Sub();
b.x();             // Base(static は参照型で決まる:隠蔽)
Sub.x();           // Sub(型名で呼ぶ)
Java

private・final メソッドはオーバーライド不可

private は子から見えず“別物”になり、final は上書き禁止なので、いずれも動的バインディングの対象になりません。動的に切り替えたい処理は、アクセス可能な非 final のインスタンスメソッドとして設計します。


オーバーライドとオーバーロードの違い(混同しない)

オーバーライドが動的バインディングの中心

親のメソッドと「同じシグネチャ(名前・引数型・並び)」を子が再定義すること。これにより、実体に応じて実装が選ばれます。@Override を付ける習慣で、意図しないズレを防ぎます。

class Greeter { String hi(String name){ return "Hi " + name; } }
class FriendlyGreeter extends Greeter {
    @Override String hi(String name){ return "Hello " + name + "!"; }
}
Greeter g = new FriendlyGreeter();
System.out.println(g.hi("Taro")); // Friendly の実装(動的)
Java

オーバーロードは静的解決

同名でも「引数が違う」別メソッドです。どれが呼ばれるかはコンパイル時に決まり、動的切り替えには関与しません。

class Greeter {
    String hi(String name){ return "Hi " + name; }
    String hi(){ return "Hi"; } // オーバーロード(静的解決)
}
Java

設計で生かす動的バインディング(テンプレート+戦略)

テンプレートメソッドで流れを固定し、差し替え点を動的に

親が処理の骨格を final で定義し、差し替えたいステップを抽象・protected にします。拡張しても骨格は壊れず、違いは動的に切り替わります。

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

ストラテジパターンで呼び出し側の分岐を消す

役割をインターフェースとして注入し、実体に応じて動的に選ばせます。追加しても呼び出し側は不変です。

final class NameService {
    private final Formatter formatter;
    NameService(Formatter f){ this.formatter = f; }
    String format(String s){ return formatter.format(s); } // 実体で動的選択
}
Java

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

is-a が成り立たない継承で契約破綻(LSP違反)

「X は Y の一種」と言えないのに継承すると、親型として扱ったときに挙動が壊れます。継承は is-a が成立する場面に限り、その他はインターフェース+委譲に切り替えます。

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

初期化中にオーバーライド可能メソッドを呼ぶと、子の未初期化フィールドへ触れて事故ります。初期化は「親の完成 → 子の完成」を厳守し、骨格はコンストラクタ外の final メソッドで固定します。

フィールド・static を混ぜて“動的のつもり”にならない

動的に切り替わるのはインスタンスメソッドだけ。フィールド値や static 呼び出しは参照型で固定されるので、切替たい処理は必ずインスタンスメソッドへ寄せます。


例題で体感する動的バインディング

例 1: 連鎖的な再利用(super)で要約を積み上げ

class Base { String info(){ return "base"; } }
class Mid extends Base {
    @Override String info(){ return super.info() + "->mid"; }
}
class Sub extends Mid {
    @Override String info(){ return super.info() + "->sub"; }
}
Base x = new Sub();
System.out.println(x.info()); // base->mid->sub(実体に応じて最下段が選ばれる)
Java

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

java.util.List<Shape> shapes = java.util.List.of(new Rect(3,4), new Circle(2));
double sum = shapes.stream().mapToDouble(Shape::area).sum(); // 分岐なしで実体に応じて動的選択
System.out.println(sum);
Java

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

動的バインディングは「インスタンスメソッドの実装選択を、実行時の実体の型で決める」仕組みで、ポリモーフィズムの心臓部です。参照型は“見える範囲”を制限するだけで、呼ばれる実装は実体で決まる。フィールド・static・オーバーロードは静的解決、動的に切替えたい処理はオーバーライド可能なインスタンスメソッドへ。テンプレート+戦略で骨格を固定しつつ、差分は動的に——この線引きを守れば、分岐が消え、拡張に強い設計が手に入ります。

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