13.

OOP 継承完全ガイド — extends・super・Composition

編集
この記事の要点
  • 継承 (Inheritance) = 既存クラスのフィールドとメソッドを引き継いで新クラスを作る仕組み
  • Java は 単一継承 (extends は 1 つ)、インターフェースは複数 implements 可
  • IS-A 関係が成り立つときだけ継承する (「Dog は Animal」○、「Stack は ArrayList」×)
  • Liskov 置換原則: 子は親の代わりに使えること
  • Composition over Inheritance: 多くの場合継承より「持つ」(has-a) 方が柔軟

継承の基本

// 親クラス (スーパークラス / 基底クラス)
public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void sleep() {
        System.out.println(name + " is sleeping");
    }
}

// 子クラス (サブクラス / 派生クラス)
public class Dog extends Animal {

    public Dog(String name) {
        super(name);   // ★ 親のコンストラクタを呼び出し
    }

    // 親のメソッドをオーバーライド
    @Override
    public void sleep() {
        System.out.println(name + " (dog) is sleeping like a log");
    }

    // 子独自のメソッド
    public void bark() {
        System.out.println(name + " says: Woof!");
    }
}

// 利用
Dog d = new Dog("Pochi");
d.sleep();  // オーバーライドされた版
d.bark();   // 子独自

// 親型変数に子を入れる (ポリモーフィズム)
Animal a = new Dog("Hachi");
a.sleep();  // 動的ディスパッチで Dog の sleep が呼ばれる
// a.bark();  // ★ コンパイルエラー (Animal 型では bark を知らない)

extends と super

キーワード意味
extends子クラスが親を継承することを宣言
super(...)親のコンストラクタ呼び出し (コンストラクタ最初の文)
super.method()親のメソッドを呼び出し
super.field親のフィールドを参照
@Overrideオーバーライド宣言 (任意だが付けるべき)
final class継承禁止
final methodオーバーライド禁止

Constructor Chaining (親 → 子 の順で実行)

class A {
    public A() { System.out.println("A()"); }
}

class B extends A {
    public B() {
        // super(); ← ★ 暗黙で挿入される (引数なしコンストラクタ呼び出し)
        System.out.println("B()");
    }
}

class C extends B {
    public C() {
        super();   // 明示
        System.out.println("C()");
    }
}

new C();
// 出力:
// A()
// B()
// C()

// 親に引数なしコンストラクタが無いと
class X {
    public X(int n) { }
}
class Y extends X {
    public Y() {
        // ★ コンパイルエラー (X の引数なしコンストラクタが無い)
        // → 明示的に super(0); 等が必要
    }
}

IS-A 関係 (継承の判断基準)

「子は親の一種である」が言えるときだけ継承を使います:

  • Dog Animal ✅
  • Manager Employee ✅
  • Square Rectangle … ✗ (有名な反例: 高さと幅を独立変更できない)
  • Stack ArrayList … ✗ (Stack に get(0) が露出するのは設計ミス → Composition 推奨)

Liskov 置換原則 (LSP)

子は親の代わりに使えなければならない。つまり、親型変数に子インスタンスを入れても期待通り動くこと:

// 古典的反例 (Square extends Rectangle)
class Rectangle {
    protected int w, h;
    public void setWidth(int w)  { this.w = w; }
    public void setHeight(int h) { this.h = h; }
    public int area() { return w * h; }
}

class Square extends Rectangle {
    // 正方形なので幅と高さを連動
    @Override
    public void setWidth(int w)  { this.w = w; this.h = w; }
    @Override
    public void setHeight(int h) { this.w = h; this.h = h; }
}

void test(Rectangle r) {
    r.setWidth(5);
    r.setHeight(3);
    assert r.area() == 15;   // ★ Square だと 9 になり契約違反
}
// → Square IS NOT a Rectangle (LSP 違反)

ダイヤモンド継承問題と Java の解決策

C++ のような多重継承では「同じ親を 2 つ持つ子」で同じメソッドのどちらが呼ばれるか曖昧になります (ダイヤモンド問題)。Java は class の多重継承を禁止し、代わりに interface (および default メソッド) を用意:

interface Flyable {
    default void move() { System.out.println("Flying"); }
}
interface Swimmable {
    default void move() { System.out.println("Swimming"); }
}

class Duck implements Flyable, Swimmable {
    // ★ どちらの default を使うか曖昧 → コンパイルエラー
    @Override
    public void move() {
        Flyable.super.move();  // 明示で解決
    }
}

protected アクセス

public class Animal {
    protected String name;    // 子からアクセス可
    private int secretId;     // 子からも見えない

    protected void onSleep() { }   // フック (子が拡張用)
}

public class Dog extends Animal {
    public void greet() {
        System.out.println(name);    // ✅ protected フィールドにアクセス
        // System.out.println(secretId); // ★ NG (private)
    }
}

final class で継承禁止

// String, Integer, LocalDate などは final
public final class Coordinate {
    private final double x, y;
    // ...
}

// public class GeoCoordinate extends Coordinate { } // ★ コンパイルエラー

// 用途:
// - イミュータブル (不変) を保証したい
// - セキュリティ上、サブクラスで挙動を変えられたくない
// - 設計者がサブクラス対応していない

Composition over Inheritance (★ 重要原則)

「is-a」ではなく「has-a」が成り立つなら、継承ではなく Composition (持つ) を使う方が柔軟

// ❌ 継承
public class CountingList<E> extends ArrayList<E> {
    private int addCount = 0;

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
        // ★ ArrayList.addAll が内部で add を呼ぶ実装だと
        //   addCount が 2 倍カウントされる (実装に依存するバグ)
    }
}

// ✅ Composition
public class CountingList<E> {
    private final List<E> inner = new ArrayList<>();
    private int addCount = 0;

    public boolean add(E e) {
        addCount++;
        return inner.add(e);
    }

    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return inner.addAll(c);
    }
}

言語別比較

言語継承記法多重継承
Javaclass B extends A不可 (interface 複数 OK)
PHPclass B extends A不可 (trait あり)
C#class B : A不可 (interface 複数 OK)
Pythonclass B(A):★ 可 (MRO で解決)
C++class B : public A★ 可 (virtual 継承)
Rubyclass B < A不可 (Mixin で実質可)

Mixin / trait

多重継承の代わりに、PHP の trait や Ruby / Python の Mixin で「振る舞いを横断的に追加」する仕組み:

trait Loggable {
    public function log(string $msg): void {
        error_log(static::class . ": $msg");
    }
}

class UserService {
    use Loggable;   // trait を取り込む
}

(new UserService())->log("created");

FAQ

Q: 継承と Composition どちらを使うべき?
A: 迷ったら Composition。IS-A が自然に成り立つ場合のみ継承。

Q: extendsimplements の違い
A: extends はクラス継承 (1 つだけ)、implements はインターフェース実装 (複数可)。クラスは extends X implements Y, Z のように両方使える。

Q: 親クラスを変更すると子に影響が出る
A: 継承の宿命 (壊れやすい基底クラス問題)。public API を慎重に設計し、安易な protected フィールド公開は避ける。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本的なルール
  2. 変数
  3. 演算子
  4. 標準ライブラリ
  5. 外部ライブラリ
  6. 制御構文
  7. リスト(配列)
  8. タプル
  9. セット
  10. 辞書(dict)
  11. クラスとメソッド
  12. 継承の概念と必要性
  13. 継承の構文
  14. コンストラクタ
  15. cookieの値の設定と取得
  16. 例外処理
  17. 例外を文字列で出力する方法
  18. httpリクエスト(curl)をする方法
  19. Responseオブジェクトの中身の確認
  20. 変数が空かどうか判定する方法
  21. タイムゾーンの設定と現在日時の取得と文字列化
  22. シングルクォーテーションとダブルクォーテーションの違い

最近更新/作成されたページ