動的バインディングとは
動的バインディングは「どのメソッド実装を呼ぶかを、実行時の“実体の型”に基づいて決める仕組み」です。参照の型が親(抽象クラスやインターフェース)でも、実際に入っているオブジェクトが子なら、子のオーバーライドが選ばれます。これにより、呼び出し側は共通の型だけに依存し、分岐を増やさずに拡張できます。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(型名で呼ぶ)
Javaprivate・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・オーバーロードは静的解決、動的に切替えたい処理はオーバーライド可能なインスタンスメソッドへ。テンプレート+戦略で骨格を固定しつつ、差分は動的に——この線引きを守れば、分岐が消え、拡張に強い設計が手に入ります。
