多段継承とは何か
多段継承は「親→子→孫…と継承が階層的に連なっている状態」を指します。AをBが継承し、BをCが継承する、といった“何段も連なる継承”です。Javaは「単一継承」なのでクラスを複数同時に extends はできませんが、段を重ねることは可能です。段が増えるほど、振る舞いの差し替え(オーバーライド)と初期化の連鎖が複雑になります。
メソッド解決とオーバーライドの連鎖(重要)
実体の型に基づく動的ディスパッチ
メソッド呼び出しは、参照型ではなく“実体の型”に基づいて最も下位のオーバーライドが選ばれます。中間クラスで上書きしていれば、さらに下位が再上書きできます。呼び出し側は常に「最後にオーバーライドした実装」が使われます。
abstract class Animal { abstract String speak(); }
class Mammal extends Animal {
@Override String speak() { return "mammal"; }
}
class Cat extends Mammal {
@Override String speak() { return "meow"; }
}
Animal a = new Cat();
System.out.println(a.speak()); // "meow"(最下段の実装)
Javasuper による段階的な再利用
子クラスのオーバーライド内で super.method() を呼ぶと、直上の親の実装を明示的に再利用できます。段が多いと「どの段の実装を呼ぶか」を意識する必要が出ます。
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"; }
}
System.out.println(new Sub().info()); // "base -> mid -> sub"
Javaコンストラクタ呼び出しと初期化順序(重要)
上から下へ連鎖する初期化
子のコンストラクタは最初の行で super(…) を呼び、親の初期化を先に完了させます。多段継承では「最上位の親から順に」初期化され、そこから子へ降りていきます。この順序を守ることで、下位は常に上位の整合性の上に成り立ちます。
class A { A() { System.out.println("A"); } }
class B extends A { B() { super(); System.out.println("B"); } }
class C extends B { C() { super(); System.out.println("C"); } }
new C(); // 出力: A → B → C(親から順に)
Javaコンストラクタでオーバーライド可能メソッドを呼ばない
親のコンストラクタから、下位がオーバーライドするメソッドを呼ぶと、下位の未初期化フィールドに触れてしまう危険があります。多段ほどリスクが増すため、初期化中の仮想メソッド呼び出しは避けるのが鉄則です。
設計指針:いつ多段にするか
本質的な階層があり、共通の骨格を段階的に固めたいとき
たとえば「動物 → 哺乳類 → 猫」のように、各段で意味のある共通ロジックや契約が増える場合は妥当です。契約(引数の前提、戻り値の意味、必須前処理)を上位段で固定し、下位段では差分を小さく保ちます。
振る舞いの差し替えポイントが適切に限定されているとき
上位段はテンプレートメソッドで処理の骨格を final で固め、protected/abstract の小さな拡張ポイントだけ開くと、下位段での誤用が減ります。段が増えても壊れにくく、読解しやすい構造を維持できます。
多段が“便宜上の再利用”になっていないかを常に点検する
「機能を使いたいから継承」ではなく、委譲(持つ・利用する)で解決できないかを検討します。段が増えるほど結合が強くなり、変更の波及が大きくなるため、必要性が明確なときだけ採用します。
例題で理解する多段継承
例 1: テンプレートメソッドで段階的にルールを追加
abstract class Importer {
public final String run(String input) {
var normalized = normalize(input);
return save(prepare(normalized));
}
protected String normalize(String s) { return s == null ? "" : s.trim(); }
protected abstract String prepare(String s);
protected abstract String save(String s);
}
abstract class CsvImporter extends Importer {
@Override protected String prepare(String s) { return s.replaceAll("\\s+", ","); }
}
class StrictCsvImporter extends CsvImporter {
@Override protected String save(String s) { return "STRICT:" + s; }
}
System.out.println(new StrictCsvImporter().run(" a b "));
// normalize(Importer) → prepare(CsvImporter) → save(StrictCsvImporter)
Javaこのように、各段が“責務の層”を追加し、骨格は上位で固定します。下位は最小限の差分だけ実装します。
例 2: 共変戻り値と super の再利用
class Animal {}
class Cat extends Animal {}
class Factory {
Animal create() { return new Animal(); }
String tag() { return "base"; }
}
class CatFactory extends Factory {
@Override Cat create() { return new Cat(); } // より具体的に
@Override String tag() { return super.tag() + "->cat"; } // 段階的に要約
}
class FancyCatFactory extends CatFactory {
@Override String tag() { return super.tag() + "->fancy"; }
}
System.out.println(new FancyCatFactory().tag()); // "base->cat->fancy"
Java段を重ねても、戻り値はより具体的に狭められ、要約は super で積み上げられます。
つまずきやすいポイントと回避策(重要)
壊れやすい基底クラス問題
上位段の仕様変更が下位段に連鎖しやすくなります。上位は骨格を final で固定し、拡張ポイントを小さく限定します。protected フィールドの露出を避け、検証付きのメソッド経由に統一します。
読解コストの増加
どの段で何が上書きされているかを追うのが難しくなります。階層は浅く保ち、命名で意図(normalize/prepare/saveなど)を明確にします。@Override を徹底して、意図せぬオーバーロードを防ぎます。
初期化順序の罠
コンストラクタでオーバーライド可能メソッドを呼ばない、super(…) は必ず先頭行で呼ぶ、といった基本を守ります。複雑な初期化はファクトリ/ビルダーに逃がします。
インターフェースとの併用で“疑似多重継承”を誤解しない
Javaはクラスの多重継承は不可です。複数の契約を持たせたいときはインターフェースを複数実装し、振る舞いは委譲で構成します。ダイヤモンド継承のような曖昧性は、インターフェースの default を明示オーバーライドして解消します。
仕上げのアドバイス(重要部分のまとめ)
多段継承は「責務を段階的に重ね、共通の骨格を上位で固定し、差分を下位で安全に表現する」ための道具です。動的ディスパッチは最下段の実装を選び、super で必要な段の処理を積み上げられます。初期化は親から子へ順を守り、コンストラクタでは仮想メソッドを呼ばない。階層は浅く、拡張ポイントは最小に、迷ったら委譲とインターフェースへ——この型を守ると、読みやすく壊れにくい継承が手に入ります。
