継承とは?オブジェクト指向での使いどころ・委譲との使い分け・アンチパターン
継承(inheritance)は、オブジェクト指向プログラミングで既存クラスの性質を引き継いで新しいクラスを作る仕組みです。コードの重複を減らせる便利な道具ですが、使い方を誤ると変更に弱く、ほどけない依存を生みます。この記事では、継承の基本と仕組みから、利点と欠点、避けるべき「悪い継承」、委譲との使い分け、そして言語による多重継承の扱いの違いまでを、実例を交えて整理します。
まとめ:継承の要点
- 継承は「子クラスが親クラスの性質を引き継ぐ」仕組みで、共通部分を親に集約してコードの重複を減らす。
- 継承が正しいのは「子は親の一種である(is-a)」が成り立つときだけ。成り立たないなら継承ではなく委譲を使う。
- 継承は親子を強く結びつけるため、安易に使うと親の変更が子に波及し、保守を難しくする。
- 多くの現場の指針は「継承よりコンポジション(委譲)を優先する」。継承は共通の型として扱いたいときに絞る。
- 多重継承の可否は言語で異なる。Java・C#は単一継承+インタフェース、C++は多重継承、PythonはMROで解決する。
継承の仕組みと is-a 関係
継承は、あるクラス(親クラス・スーパークラス)が持つ変数やメソッドを、別のクラス(子クラス・サブクラス)が受け継ぐ仕組みです。子クラスは親の機能をそのまま使えるうえ、独自の機能を追加したり、受け継いだ振る舞いを上書きしたりできます。
継承が成立する条件は、子クラスが親クラスの「一種」だと言えること、いわゆる is-a 関係です。「犬は動物の一種である」は成り立つので、Animalを親、Dogを子にするのは自然です。次のように、共通の性質を親に置き、差分だけを子に書きます。
class Animal {
String name;
void breathe() { ... }
}
class Dog extends Animal {
void bark() { ... }
}
Dogは自分でbreatheを書かなくても、Animalから受け継いで使えます。共通処理を一箇所にまとめられるのが、継承の最も基本的な効きどころです。
継承の利点と欠点
継承の利点は、まず重複の削減です。共通のコードを親に集約するため、同じ処理を何度も書かずに済み、修正も親を直せば全体に反映されます。さらに、子クラスをすべて親クラスの型として扱えるため、種類の異なるオブジェクトを一様に扱う設計(この一様な扱いがポリモフィズムです)の土台になります。
一方で欠点も明確です。継承は親と子を強く結びつけます。親クラスの実装を変えると、意図せず全部の子クラスの挙動が変わることがあります。これを「基底クラスの脆弱性」と呼びます。継承関係が深く積み重なると、ある振る舞いがどのクラスに由来するのかを追いにくくなり、コードを読む負担が増えます。便利さと引き換えに、変更への弱さを抱え込むわけです。
避けるべき悪い継承とアンチパターン
継承の失敗は、たいてい is-a が成り立たないのに継承を使うことから始まります。よくあるのが「コードを再利用したいから」という理由だけで継承するケースです。たとえば、スタックを実装するのにListクラスを継承すると、スタックには不要な挿入・削除メソッドまで外部に露出し、本来禁じたい操作ができてしまいます。スタックはListの一種ではないので、これは誤った継承です。
もう一つの典型が、正方形クラスを長方形クラスから継承する例です。数学上は正方形は長方形の一種ですが、「幅と高さを独立に変えられる」という長方形の振る舞いを正方形は満たせず、置き換えると破綻します(リスコフの置換原則違反)。判断基準は一つです。「子を親として扱っても、どんな場面でも問題が起きないか」を満たせないなら、継承にしてはいけません。再利用が目的なら、次に述べる委譲を選びます。
迷ったときは、次のどれかに当てはまるなら継承をやめて委譲を検討します。
- 「子は親の一種だ(is-a)」と自然に言えず、再利用だけが目的になっている。
- 親の公開メソッドに、子では成り立たない・使わせたくないものが含まれる。
- 子を親として渡した途端に前提が崩れる(リスコフの置換原則を満たせない)。
- 継承の階層が三段以上に深く、振る舞いの由来が追いにくい。
継承と委譲(コンポジション)の使い分け
委譲(コンポジション)は、あるクラスが別のクラスを部品として内部に持ち、必要な処理をその部品に任せる方法です。関係で言えば has-a、「AはBを持っている」です。先のスタックの例なら、Listを継承するのではなく内部にListを一つ持ち、pushやpopの中で必要なメソッドだけを呼び出します。こうすれば、公開したくない操作を隠したまま、Listの機能を再利用できます。
両者の選び分けは関係で決めます。「子は親の一種か(is-a)」なら継承、「AはBを持っている/使っているだけ(has-a)」なら委譲です。多くの設計指針が「継承よりコンポジションを優先せよ」と述べるのは、委譲の方が結合が緩く、変更に強いからです。継承は、共通の型として一様に扱いたい積極的な理由があるときに絞って使うのが安全です。
多重継承と言語ごとの扱いの違い
一つの子クラスが複数の親クラスを継承することを多重継承と呼びます。これを認めるかは言語で分かれます。複数の親が同じ名前のメソッドを持つと、どちらを受け継ぐか曖昧になる「ダイヤモンド問題」が起きるためです。
JavaとC#は、クラスの多重継承を禁止し、単一継承に限定します。その代わりインタフェース(C#ではインターフェース)を複数実装することで、多重継承に近い柔軟さを型安全に得られます。C++はクラスの多重継承を認め、曖昧さは仮想継承などで制御します。Pythonも多重継承を認め、どのメソッドを使うかをMRO(メソッド解決順序、C3線形化)という規則で一意に決めます。なお、Javaのrecordのように、そもそも継承の親になれない・できないクラスもあります(詳しくはレコードクラスの解説を参照)。多重継承を扱うときは、使う言語がどの方式かをまず確認する。ここを取り違えると、設計そのものが成り立たなくなります。
オーバーライドと継承の関係
「オーバーライドと継承の違い」を問われることがありますが、両者は対立概念ではありません。オーバーライドは、子クラスが親クラスから受け継いだメソッドを、自分用に定義し直すことです。つまり継承という仕組みの中で使われる一機能です。Dogが親のmoveを受け継ぎつつ「走る」挙動に上書きすれば、それがオーバーライドです。似た用語のオーバーロードは、同じ名前で引数の異なるメソッドを複数用意することで、継承とは別物です。混同しやすいので、オーバーライド=継承したメソッドの再定義、と押さえておくと整理できます。
継承・カプセル化・ポリモフィズムの関係
継承は、オブジェクト指向の三本柱の一つです(抽象化を加えて四大原則と数える立場もあります)。残りの二つと役割が違うため、混同しないよう整理しておきます。カプセル化はデータと操作をひとまとめにして内部を隠す仕組み、ポリモフィズムは異なるクラスのオブジェクトを同じ型として一様に扱う仕組みです。継承は、このポリモフィズムを実現する土台の一つになります。それぞれの詳細は、ポリモフィズム(多態性)の解説とカプセル化の解説にまとめています。本記事は、三本柱のうち継承に絞って扱いました。
よくある質問
継承とは一言でいうと何ですか?
既存のクラス(親)の変数やメソッドを、別のクラス(子)が引き継いで使える仕組みです。共通部分を親にまとめ、差分だけを子に書くことで、コードの重複を減らせます。
継承と委譲の違いは何ですか?
継承は「子は親の一種(is-a)」の関係、委譲は「AはBを持っている(has-a)」の関係で使います。再利用だけが目的なら、結合の緩い委譲が安全です。
オーバーライドと継承は何が違いますか?
継承は親の機能を引き継ぐ仕組み全体、オーバーライドはその中で親から継いだメソッドを子で定義し直す操作です。オーバーライドは継承の一機能で、対立する概念ではありません。
多重継承ができる言語とできない言語は?
C++やPythonはクラスの多重継承ができます。JavaとC#はクラスでは単一継承のみで、複数のインタフェースを実装することで補います。
継承はなぜ避けるべきと言われるのですか?
親子が強く結びつき、親の変更が子に波及して保守が難しくなるためです。is-aが成り立たない再利用目的の継承は特に危険で、その場合は委譲を使います。