アーキテクチャ

継承とは?ポリモーフィズムとの違い・委譲との使い分けをJavaとPythonのコードで解説

継承とは?ポリモーフィズムとの違い・委譲との使い分けをJavaとPythonのコードで解説

継承(inheritance)は、既存のクラスが持つ実装と型を、新しいクラスがそのまま引き継ぐ仕組みです。ポリモーフィズムと並べて語られることが多いものの、両者は別の層の概念です。継承は「クラスどうしの関係」、ポリモーフィズムは「呼び出す側から見た性質」で、継承はポリモーフィズムを作る手段の一つにすぎません。この記事では、両者の違いを比較表とコードで整理したうえで、継承を使わずにポリモーフィズムを得る方法、継承しても多態にならない落とし穴、委譲へ切り替える判断基準までを扱います。掲載例の検証対象は、JavaがJDK 25.0.4.1、PythonがPython 3.9.6です。各例に出力を併記しています。

まとめ:継承とポリモーフィズムの違いと使い分けの要点

  • 継承は「子クラスが親クラスの実装と型を引き継ぐ」仕組み。ポリモーフィズムは「同じ呼び出しで、実体の型に応じた処理が動く」性質。
  • 継承はポリモーフィズムの手段の一つで、インタフェース・ダックタイピング・ジェネリクスでも多態は作れる。
  • Javaではフィールド参照とstaticメソッド呼び出しは実行時の型によって切り替わらない。インスタンスメソッドの呼び出しでは、実行時の型に応じてオーバーライド先が選ばれる。
  • 継承は「子は親の一種(is-a)」が成り立ち、親として渡しても壊れないときだけ使う。再利用だけが目的なら委譲を選ぶ。
  • Kotlinはクラスが既定でfinal、Javaは17からsealedで継承先を限定できる。Pythonの typing.final は型チェッカー向けで、実行時の継承は止めない。

継承の仕組み:extendsで引き継がれるもの

継承では、親クラス(スーパークラス)のフィールドとメソッドを子クラス(サブクラス)が受け継ぎ、子は差分だけを書きます。Javaでは extends、Pythonではクラス定義の括弧に親を書きます。Javaの各例は独立した抜粋です。実行時は必要な import java.util.*; を追加し、型定義とは別に Main クラスを作って、実行文を public static void main(String[] args) 内に置きます。ファイル名は Main.java とし、javac Main.java、java Main の順で実行してください。

class Animal {
    String speak() { return "..."; }
}
class Dog extends Animal {
    @Override String speak() { return "ワン"; }
}

このとき子が受け取るのは2つです。1つは実装(親のメソッドの中身をそのまま使える)、もう1つは型(Dog を Animal 型の変数に入れられる)。継承の利点と欠点はどちらもこの「実装と型を同時に受け取る」点から来ます。Javaではコンストラクタは継承されず、子のコンストラクタが親のコンストラクタを呼びます(書かなければ引数なしの super() が補われます)。Java 25からはFlexible Constructor Bodiesにより、その呼び出しより前に引数の検証などの文も書けます。ただし構築中のインスタンスのフィールドを読むことはできません。private なメンバーは子から直接は触れません。

継承とポリモーフィズムの違い

継承は「どのクラスがどのクラスを土台にしているか」という定義側の関係です。ポリモーフィズムは「呼び出す側が相手の具体的な型を知らなくても、相手に合った処理が選ばれる」という使う側の性質です。継承を使うと、子を親の型として扱えるので、結果としてポリモーフィズムが得られます。ただし、それは継承の効果の一部であって、両者は同じものではありません。

観点 継承 ポリモーフィズム
何か クラス間の関係 呼び出し側から見た性質
見る場所 クラスの定義(extends) メソッドを呼ぶ箇所
引き継ぐもの 実装と型 何も引き継がない
成り立つ条件 is-a 関係 共通の呼び出し口があること
継承なしで可能か — 可能(インタフェース等)

ポリモーフィズムそのものの種類や読み方はポリモーフィズム(ポリモフィズム・多態性)の解説で扱っています。ここでは、継承との境目に絞ります。

継承を使わないポリモーフィズム:インタフェース・ダックタイピング・ジェネリクス

型理論の古典である Cardelli と Wegner の論文「On Understanding Types, Data Abstraction, and Polymorphism」(1985年)は、ポリモーフィズムを大きく2つに分けています。多くの型に同じコードが働く universal(パラメトリック多相と包含多相)と、型ごとに別のコードが働く ad hoc(オーバーロードと型変換)です。継承によるサブタイプの多態はこのうち包含多相の1つにあたります。言い換えると、継承はポリモーフィズムの一部しか担っていません。

ここでいう「継承なし」はクラスの実装継承を使わないという意味です。Javaでは、互いに親子関係のないクラスでも、同じインタフェースを実装すれば、そのインタフェース型として扱えます。

interface Shape { double area(); }
record Circle(double r) implements Shape {
    public double area() { return Math.PI * r * r; }
}
record Square(double side) implements Shape {
    public double area() { return side * side; }
}

for (Shape sh : List.of(new Circle(1), new Square(2)))
    System.out.printf("%s %.2f%n", sh.getClass().getSimpleName(), sh.area());
Circle 3.14
Square 4.00

record はクラスを継承できない(暗黙に java.lang.Record を継承する)ので、ここでの多態はインタフェースだけで成り立っています。Pythonでは共通の親もインタフェース宣言も要りません。同じ名前のメソッドを持っていれば呼べるダックタイピングです。

class Duck:
    def speak(self): return "ガーガー"
class Robot:
    def speak(self): return "ピー"
for x in (Duck(), Robot()):
    print(type(x).__name__, x.speak())
Duck ガーガー
Robot ピー

Goは型の継承をそもそも持たず、公式FAQでもその理由を説明しています。Goのインタフェースは宣言なしで満たされる暗黙実装で、詳しくはGoのインターフェースの解説にまとめています。多態が欲しいだけなら、継承は必須ではありません。

継承してもポリモーフィズムにならないもの:フィールドとstaticメソッド

反対に、継承していても多態にならない場面があります。Javaでは、子が親と同じ名前のフィールドやstaticメソッドを定義しても、それはオーバーライドではなく「隠蔽(hiding)」です。どちらが使われるかは実体ではなく変数の型で決まります。

class Animal {
    String name = "animal";
    static String kind() { return "Animal.kind"; }
    String speak() { return "..."; }
}
class Dog extends Animal {
    String name = "dog";
    static String kind() { return "Dog.kind"; }
    @Override String speak() { return "ワン"; }
}

Animal a = new Dog();
System.out.println(a.speak() + " / " + a.name + " / " + a.kind());
ワン / animal / Animal.kind

実体は Dog なのに、多態になったのは speak() だけで、name と kind() は親のものが返りました。javacは a.kind() に -Xlint:static で「static method should be qualified by type name」と警告します。継承したから多態になる、とは限らないということです。子で同名のフィールドを定義しない、staticメソッドはクラス名で呼ぶ、の2点を守れば、この食い違いは起きません。

オーバーライド・インタフェースと継承の違い

オーバーライドはメソッドの再定義、インタフェースは共通の型契約という観点から、クラス継承との関係を整理します。

オーバーライドは継承の中で使う操作

オーバーライドは、子が親から受け継いだインスタンスメソッドを定義し直すことです。継承という仕組みの中で使う操作なので、両者は対立する概念ではありません。継承は「受け継ぐ」こと全体、オーバーライドは「受け継いだ振る舞いを差し替える」ことです。上の例の speak() がそれにあたり、ポリモーフィズムが生まれるのもこの差し替えの部分です。似た名前のオーバーロードは、同じ名前で引数の型や数が違うメソッドを並べることで、継承が無くても成り立ちます。どれが呼ばれるかはコンパイル時に決まります。

インタフェースの型契約とdefaultメソッドの実装継承

クラスの継承が実装と型の両方を引き継ぐのに対し、インタフェースの実装は基本的に型(守るべき呼び出し口)だけを引き継ぎます。Javaでは直接の親クラスは1つですが、複数のインタフェースを実装できます。ただし、メソッドの戻り値型やdefault実装などの競合があれば、解消が必要です。インタフェースの意味や種類はインターフェースの解説を参照してください。

ただしJava 8からはインタフェースに default メソッドで実装を書けるため、実装も少しだけ継承されます。同じシグネチャの default メソッドを持つ無関係な2つのインタフェースを実装すると、コンパイルエラーになります。

interface Flyer   { default String move() { return "fly"; } }
interface Swimmer { default String move() { return "swim"; } }
class Duck implements Flyer, Swimmer { }
error: types Flyer and Swimmer are incompatible;
  class Duck inherits unrelated defaults for move() from types Flyer and Swimmer

解消するには、クラス側で move() をオーバーライドし、Flyer.super.move() のように使う方を明示します。どちらを採るかを曖昧にさせず、実装クラスに決めさせる設計です。

継承と委譲の使い分け

コンポジションは、別のオブジェクトを部品として保持して組み合わせる設計です。その部品に処理を任せることを、本記事では広い意味で「委譲」と呼びます。関係で言えば has-a(AはBを持っている)です。GoFの『Design Patterns』(1994年)は「クラスの継承よりオブジェクトのコンポジションを優先せよ(Favor object composition over class inheritance)」を設計原則に挙げ、Joshua Bloch の『Effective Java』第3版も項目18を「継承よりコンポジションを選ぶ」に充てています。理由は、次の2つの実例でわかります。

親の内部実装に依存して壊れる例:InstrumentedHashSet

『Effective Java』の項目18にある例(著者公開のソースコード)を、要素を追加した回数を数える HashSet の子クラスとして書くと次のようになります。

class InstrumentedHashSet<E> extends HashSet<E> {
    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);
    }
}

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("a", "b", "c"));
System.out.println("InstrumentedHashSet addCount=" + s.addCount);
InstrumentedHashSet addCount=6

3件追加したのに6になりました。親の addAll が内部で add を1件ずつ呼んでおり、それがオーバーライド済みの add に戻ってきて二重に数えたためです。HashSet が受け継いでいる AbstractCollection.addAll のJavadoc には、「この実装は」と断ったうえで要素を1件ずつ追加すると書かれています。つまり子は、親が「今の実装では」こう動くという前提に依存しています。親が addAll を独自の処理に書き換えれば、同じ子クラスの結果は3に変わります。これが「基底クラスの脆弱性(fragile base class)」です。同じ処理を、HashSet をフィールドに持つ委譲で書くと3になります。

class CountingSet<E> {
    private final Set<E> s = new HashSet<>();
    int addCount = 0;
    boolean add(E e) { addCount++; return s.add(e); }
    boolean addAll(Collection<? extends E> c) { addCount += c.size(); return s.addAll(c); }
}

次の実行文で3要素の追加を試み、計数値を出力します。

CountingSet<String> cs = new CountingSet<>();
cs.addAll(List.of("a", "b", "c"));
System.out.println("CountingSet addCount=" + cs.addCount);
CountingSet addCount=3

使わせたくない操作まで公開される例:java.util.Stack

JDK自身にも、継承を選んで後から勧められなくなったクラスがあります。java.util.Stack は Vector を継承しているため、スタックには不要な位置指定の挿入まで呼べてしまいます。

Stack<Integer> st = new Stack<>();
st.push(1); st.push(2); st.push(3);
st.add(0, 99);
System.out.println("Stack=" + st + " pop=" + st.pop());
Stack=[99, 1, 2, 3] pop=3

スタックの底に割り込めるのは、「スタックは Vector の一種」という継承が is-a として成り立っていないからです。JDK 25のStackのJavadocも、より完全で一貫したLIFO操作は Deque インタフェースとその実装が提供すると書き、ArrayDeque を勧めています。新しいコードでスタックが要るなら Deque<Integer> st = new ArrayDeque<>(); を使います。

委譲を選ぶか継承を選ぶかの判断基準

判断は関係で決めます。「子は親の一種か(is-a)」なら継承の候補、「AはBを持っている・使っているだけ(has-a)」なら委譲です。is-a に見えても、次のどれかに当てはまるなら継承をやめて委譲にします。

  • 再利用だけが目的で、子を親の型として扱う場面が無い。
  • 親の公開メソッドに、子では成り立たない・使わせたくないものがある(Stack の add(int, E))。
  • 子を親として渡すと前提が崩れる。正方形を長方形の子にすると「幅だけ変える」が成り立たない、というリスコフの置換原則違反がこれにあたります。原典のLiskov と Wing の論文(1994年)は、親の型について成り立つ性質が子の型でも成り立つことをサブタイプの条件にしています。
  • 親が別チームや外部ライブラリのクラスで、内部の呼び出し関係を保証されていない(InstrumentedHashSet の失敗)。
  • 継承階層をたどっても、ある振る舞いの定義元やオーバーライドによる影響範囲を把握できない。

逆に、継承を選んでよいのは、親が継承される前提で設計・文書化されているときです。AbstractList のような抽象クラスや、共通の手順を親が決めて一部だけ子に書かせるテンプレートメソッドパターンがその例です。同じ目的を委譲で組むとStrategyパターンになります。変更に強いのは、拡張点を差し替え可能にしておく設計で、これはオープンクローズドの原則(OCP)の考え方とも重なります。深い継承階層を抱えた既存システムの改修や作り直しは、フルスクラッチ開発の相談としてもお受けしています。

言語ごとの継承の扱い:多重継承・既定final・sealed

多重継承では、複数の親が持つ同名メソッドの解決規則が必要になります。特に、共通の祖先へ複数の継承経路がある構造を菱形継承と呼びます。言語はこれを禁止するか、解決規則を決めるかのどちらかで扱います。継承を許す既定もまちまちで、Kotlinはクラスを既定で閉じ、Java・C#・Pythonは既定で開いています。

言語 クラスの多重継承 継承の既定 継承先の限定
Java 不可(インタフェースは複数可) 継承可 final・sealed(17〜)
C# 不可(インターフェイスは複数可) 継承可 sealed
C++ 可(virtual継承で共有) 継承可 final(C++11〜)
Python 可(MROで解決) 継承可 言語の強制なし
Kotlin 不可 final(open で許可) sealed
Go 型の継承なし — 埋め込みで代替

Pythonの多重継承は、メソッドを探す順番をMRO(メソッド解決順序)で一意に決めます。Python 2.3から使われているC3線形化という規則で、Python公式ドキュメントの解説に経緯と計算方法があります。菱形の形で確かめると次のとおりです。

class A:
    def hello(self): return "A"
class B(A):
    def hello(self): return "B>" + super().hello()
class C(A):
    def hello(self): return "C>" + super().hello()
class D(B, C):
    pass

print([k.__name__ for k in D.__mro__])
print(D().hello())
['D', 'B', 'C', 'A', 'object']
B>C>A

B の中の super() が、親の A ではなくMROで次にある C を呼んでいる点に注意してください。Pythonの super() は「親クラス」ではなく「MROの次のクラス」を指します。多重継承を書くなら、D.__mro__ を一度出力して順番を確かめるのが確実です。

Javaの sealed はJEP 409でJava 17から正式機能になり、直接の継承・実装先を限定します。許可先は permits で指定するほか、同じソースファイル内の宣言から推論される場合もあります。許可先が non-sealed なら、その先の継承は開放されます。一覧に無いクラスで実装しようとすると、JDK 25では「class is not allowed to extend sealed class: Shape (as it is not listed in its ‘permits’ clause)」でコンパイルが止まります。Kotlinは公式ドキュメントのとおりクラスが既定で final で、継承させるクラスにだけ open を付けます。継承を制限する方法と強制力は言語ごとに異なります。JavaやKotlinではfinalなどを使い、Pythonでは型チェッカー向けの typing.final や文書化で継承禁止の意図を伝えます。typing.final 自体は実行時の継承を禁止しません。Javaでのrecord・sealedを使った設計はオブジェクト指向の三本柱をJavaで解説した記事にあります。

継承・カプセル化・ポリモーフィズムの関係

継承・カプセル化・ポリモーフィズムは、オブジェクト指向の三本柱として並べて説明されます(抽象化を加えて四本とする説明もあります)。カプセル化はデータと操作をひとまとめにして内部を隠すこと、ポリモーフィズムは呼び出し側が型を区別せずに済むことで、継承は後者を作る手段の一つです。ただし継承は親の内部へ子を依存させるため、使いすぎるとカプセル化を崩します。InstrumentedHashSet が壊れたのは、親が隠しておくべき内部の呼び出し関係に子が依存したからでした。カプセル化の詳細はカプセル化の解説を参照してください。

よくある質問

継承とポリモーフィズムの違いは何ですか?

継承はクラス間の関係で、子クラスが親クラスの実装と型を引き継ぐ仕組みです。ポリモーフィズムは呼び出し側から見た性質で、同じ呼び出しで実体の型に応じた処理が動くことを指します。継承はポリモーフィズムを作る手段の一つで、インタフェースやダックタイピングでも多態は作れます。

オーバーライドと継承の違いは何ですか?

継承は親の機能を引き継ぐ仕組み全体で、オーバーライドはその中で親から受け継いだインスタンスメソッドを子で定義し直す操作です。対立する概念ではありません。Javaではフィールドとstaticメソッドはオーバーライドされず隠蔽になります。

インターフェースと継承の違いは何ですか?

クラスの継承は実装と型の両方を引き継ぎ、インタフェースの実装は基本的に型(呼び出し口)だけを引き継ぎます。Javaでクラスの継承は1つまでですが、インタフェースは複数実装できます。Java 8以降は default メソッドで実装も持てますが、同じ default が衝突した場合は実装クラスでオーバーライドが必要です。

継承と委譲のどちらを使うべきですか?

子が親の一種(is-a)で、親として渡しても壊れないなら継承、それ以外は委譲です。再利用だけが目的の継承は、親の内部実装の変更で子が壊れる原因になります。迷ったら委譲を選び、共通の型が要るならインタフェースを足します。

多重継承ができる言語はどれですか?

C++とPythonはクラスの多重継承ができます。Pythonは呼び出す順番をMRO(C3線形化)で決めます。Java・C#・Kotlinはクラスの多重継承ができず、インタフェースを複数実装して補います。Goには型の継承そのものがありません。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  2. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  3. 2026.10.27 テックブログ Brotli 脆弱性:CVE-2026-111497の影響確認とNode.js・Python・.NETの更新手順
  4. 2026.01.30 コラム 税務調査は個人と法人で何が違う?担当部署・件数・調べられる点と追徴税額を比較【令和6事務年度】
  5. 2026.09.26 コラム 価格転嫁とは?価格交渉促進月間の調査に回答義務はあるか・取適法の協議記録まで

RELATED POSTS 関連記事

目次