タイトル: 継承の概念と必要性
SEOタイトル: 継承 (Inheritance) の概念と必要性完全ガイド
| この記事の要点 |
|
継承とは何か
継承 (Inheritance) は既存のクラスを土台に、新しいクラスを派生させる仕組みです。親クラス(スーパークラス / ベースクラス)が持つフィールドやメソッドを、子クラス(サブクラス / 派生クラス)が自動的に引き継ぎます。
クラスの関係はis-a 関係で表現します。「犬は動物である (Dog is-a Animal)」「正方形は四角形である (Square is-a Rectangle)」など。
// 親クラス
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating");
}
public void sleep() {
System.out.println(name + " is sleeping");
}
}
// 子クラス: Animal を継承
class Dog extends Animal {
public Dog(String name) {
super(name); // 親のコンストラクタを呼ぶ
}
// 子独自のメソッド
public void bark() {
System.out.println(name + " says: Woof!");
}
}
// 利用
Dog dog = new Dog("Pochi");
dog.eat(); // 親から継承
dog.sleep(); // 親から継承
dog.bark(); // 子独自
継承の必要性: なぜ必要か
| メリット | 説明 |
|---|---|
| コード再利用 (DRY) | 共通機能を親に集約。同じコードを書かなくていい |
| 多態性 (Polymorphism) | 親型変数で子インスタンスを扱える → 抽象的な設計 |
| 拡張性 | 既存コードを変更せず新機能を追加できる (OCP) |
| 分類 / モデル化 | 現実世界の階層構造をコードで表現 |
| 型階層 | コンパイル時の型チェックで安全 |
メソッドのオーバーライド
子クラスは親のメソッドを上書き (override) できます。これが多態性の基礎。
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}
// 多態性
Animal[] animals = { new Dog(), new Cat() };
for (Animal a : animals) {
a.makeSound(); // 実体に応じて Woof / Meow が呼ばれる
}
Liskov Substitution Principle (LSP)
SOLID 原則の L。子クラスは親クラスと置き換え可能でなければならないという原則。継承を正しく使うための指針です。
有名な反例: 数学的には正方形 is-a 長方形だが、Square extends Rectangle にすると setWidth/setHeight の挙動が壊れる → LSP 違反。
// ❌ LSP 違反の例
class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) { width = w; height = w; } // ★ 親と挙動が違う
@Override
public void setHeight(int h) { width = h; height = h; }
}
// 問題: Rectangle 型で受けても Square だと予期しない動作
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
System.out.println(r.getArea()); // 期待: 50, 実際: 100
多重継承の禁止と代替
Java / C# / Kotlin は多重継承禁止(C++/Python は許可)。理由はダイヤモンド継承問題: 共通の祖先を持つ 2 つの親から継承すると、メンバ参照が曖昧になります。
| 言語 | 多重継承 | 代替手段 |
|---|---|---|
| Java | 禁止 | interface(default メソッド可) |
| C# | 禁止 | interface(default 実装可、C# 8+) |
| Kotlin | 禁止 | interface |
| PHP | 禁止 | trait(Mixin 相当) |
| C++ | 許可 | virtual 継承で曖昧性回避 |
| Python | 許可 | MRO (C3 線形化) |
| Ruby | 禁止 | module include (Mixin) |
PHP の trait (Mixin)
// trait: 複数クラスで共有するメソッド集
trait Loggable {
public function log(string $msg): void {
echo '[' . static::class . '] ' . $msg . PHP_EOL;
}
}
trait Cacheable {
public function cache(string $key, $value): void {
// ...
}
}
class UserService {
use Loggable, Cacheable;
}
(new UserService())->log('hello');
// → [UserService] hello
super と final
class Parent {
protected int value;
public Parent(int v) { this.value = v; }
public void show() {
System.out.println("Parent: " + value);
}
}
class Child extends Parent {
public Child(int v) {
super(v); // ★ 親のコンストラクタ呼出
}
@Override
public void show() {
super.show(); // ★ 親メソッドを明示的に呼ぶ
System.out.println("Child: " + value);
}
}
// final で継承禁止
final class Utility { } // これは継承できない
// class Sub extends Utility { } // ❌ コンパイルエラー
class Foo {
public final void critical() { } // メソッド単位でオーバーライド禁止も可
}
Composition Over Inheritance
近年の設計原則は継承より委譲(コンポジション)を優先。深い継承階層は Fragile Base Class(壊れやすい親クラス)問題を生みます。
// ❌ 継承で機能追加(密結合)
class Logger { void log(String m) { ... } }
class UserService extends Logger {
void register(User u) {
log("registering"); // ロガー機能を継承で取得
// ...
}
}
// 問題: Logger を変更すると UserService にも影響、テストしにくい
// ✅ コンポジション(疎結合)
class UserService {
private final Logger logger; // 委譲
public UserService(Logger logger) {
this.logger = logger; // DI で受け取る
}
void register(User u) {
logger.log("registering");
}
}
// 利点: Logger を差し替えやすい、モック注入でテスト容易、is-a でなく has-a が自然
継承の落とし穴
- Fragile Base Class: 親クラスの変更が全子クラスに波及。深い階層ほど影響範囲が読めない
- 密結合: 子は親の内部実装に依存しがち。リファクタが困難に
- is-a でないものまで継承: 「Stack extends ArrayList」は LSP 違反(要素削除順が違う)
- protected の濫用: 子からアクセス可能 = 設計の自由度が下がる
- 菱形継承: 多重継承可能な言語では特に注意
いつ継承を使うか
| 使うべき | 避けるべき |
|---|---|
| 明確な is-a 関係がある | 機能の借用目的だけ |
| 親が抽象クラス / 安定した API | 親が頻繁に変わる |
| LSP を満たせる | 子で親の挙動を「打ち消す」設計 |
| 深さ 2-3 階層まで | 5 階層以上の深い継承 |
| テンプレートメソッドパターン等 | コードの再利用だけが目的 |
FAQ
Q: 継承と interface の使い分けは?
A: 「実装の共有」が目的なら継承、「型 (契約) の共有」が目的なら interface。Java 8+ の default メソッドで interface でも実装共有可能になりました。
Q: protected と private の使い分け
A: 子クラスからアクセスさせたいなら protected、内部実装で隠したいなら private。基本は private を優先(カプセル化)。
Q: 多重継承が許される C++/Python では何に注意?
A: C++ は virtual 継承でダイヤモンド回避、Python は MRO (Method Resolution Order) で __mro__ を確認。基底クラスはなるべく Mixin に近い小さなものに。