ポリモーフィズム(ポリモフィズム)は、同じメソッド呼び出しに対して、受け取ったオブジェクトの型ごとに違う処理が動く仕組みです。日本語では多態性(たたいせい)または多相性(たそうせい)と訳します。この記事では、意味と読み方、3つの分類、Javaでの実装、ポリモーフィズムが効かない場面、if分岐との使い分けを、JDK 25で実行を確かめたコードで説明します。
まとめ:ポリモーフィズムの要点
ポリモーフィズムは「呼び出す側のコードを変えずに、型ごとの処理を差し替えられる」性質です。ポリモフィズム・ポリモルフィズムはどれも英語polymorphismの表記ゆれで、意味は同じです。
- 分類は3つです。オーバーライドによるサブタイプ多相(実行時に決まる)、ジェネリクスによるパラメトリック多相、オーバーロードによるアドホック多相(コンパイル時に決まる)に分かれます。オブジェクト指向で単に「ポリモーフィズム」と言えば、普通はサブタイプ多相を指します。
- Javaではインターフェースをimplementsする方法と、クラスを継承してメソッドをオーバーライドする方法で実装します。
- フィールド、staticメソッド、オーバーロードの選択は、実行時の型ではなく変数の宣言型で決まります。ここを取り違えるのが典型的なバグです。
- 処理を差し替える型が今後も増えるならポリモーフィズムを使います。型の種類が固定で、型に対する処理のほうが増えるなら、Java 21以降はsealedとswitchのパターンマッチングのほうが向いています。
ポリモーフィズムの意味と読み方
定義:同じ呼び出しで型ごとに違う処理が動く仕組み
語源はギリシャ語のpoly(多くの)とmorph(形)で、「1つの呼び出しが多くの形を取る」という意味です。たとえば犬と猫がどちらも鳴くメソッドを持っていれば、呼び出す側は相手を区別せず「鳴け」と命じるだけで済みます。犬なら「ワン」、猫なら「ニャー」が返ります。新しくカラスを足しても、呼び出す側のコードは1行も変わりません。
カプセル化・継承と並んで、オブジェクト指向の三本柱の1つに数えられます(抽象化を足して4原則とする説明もあります)。三本柱全体の実装手順はオブジェクト指向とは?三本柱の実装手順とrecord・sealedまでJavaコードで解説で扱っています。
読み方と表記ゆれ・和訳(多態性・多相性)
英語polymorphismの読み方は「ポリモーフィズム」です。「ポリモフィズム」「ポリモルフィズム」も同じ語の表記ゆれで、意味の違いはありません。和訳は多態性(たたいせい)と多相性(たそうせい)の2つが使われます。プログラミング言語の入門書やJavaの文脈では多態性、型理論の文脈では多相性(パラメトリック多相、アドホック多相など)と書かれることが多いです。
ポリモーフィズムの3分類(サブタイプ・パラメトリック・アドホック)
型理論では、ポリモーフィズムを3つに分けます。Christopher Stracheyが1967年の講義に基づき、2000年にHigher-Order and Symbolic Computation誌で再刊された「Fundamental Concepts in Programming Languages」でアドホック多相とパラメトリック多相を区別し、Luca CardelliとPeter Wegnerが1985年の論文「On Understanding Types, Data Abstraction, and Polymorphism」でサブタイプ多相(包含多相、inclusion)を加えて整理しました。なお同論文は、アドホック多相にオーバーロードのほか型の自動変換(coercion)も含めています。
| 分類 | Javaでの実現 | 呼び先が決まる時点 | 別名 |
|---|---|---|---|
| サブタイプ多相 | オーバーライド | 実行時 | 動的ポリモーフィズム |
| パラメトリック多相 | ジェネリクス | コンパイル時(型は実行時に消去) | 総称性 |
| アドホック多相 | オーバーロード | コンパイル時 | 静的ポリモーフィズム |
サブタイプ多相(オーバーライド)
親クラスやインターフェースの型で受け取り、実体のクラスがオーバーライドしたメソッドを動かす方式です。どの実装が呼ばれるかは、変数の宣言型ではなく実行時のオブジェクトの型で決まります(Java言語仕様 15.12.4.4は、呼び出すメソッドの探索を対象オブジェクトの実行時クラスから始めると定めています)。Javaでは、private以外のインスタンスメソッドは、C#のvirtualのような指定をしなくてもこの方式で呼ばれます(super.で呼んだ場合を除く)。
パラメトリック多相(ジェネリクス)
型をパラメータにして、1つのコードをどの型にも同じ動きで使えるようにする方式です。JavaのList<T>や<T> T first(List<T> xs)がこれにあたります。型パラメータの宣言はJava言語仕様 4.4が定めています。サブタイプ多相と違い、型ごとに処理を変えるのではなく、どの型にも同じ処理を適用します。
アドホック多相(オーバーロード)
同じ名前のメソッドを引数の型や数を変えて複数定義する方式です。どれを呼ぶかはコンパイル時に引数の型で決まります(Java言語仕様 15.12.2)。
class Calc {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
}
add(1, 2)はint版、add(1.5, 2.0)はdouble版が選ばれます。名前の似たオーバーライドとは、呼び先が決まるタイミングが違います。
Javaでポリモーフィズムを実装する
Javaでサブタイプ多相を書く方法は、インターフェースを実装する方法とクラスを継承する方法の2つです。以下のコードはどちらもJDK 25でコンパイルと実行を確かめています。
インターフェースとimplementsで実装する
インターフェースで「実装すべきメソッド」だけを決め、各クラスがimplementsで中身を書きます。
import java.util.List;
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; }
}
public class Main {
public static void main(String[] args) {
List<Shape> shapes = List.of(new Circle(1.0), new Square(2.0));
for (Shape s : shapes) {
System.out.printf("%s %.2f%n", s.getClass().getSimpleName(), s.area());
}
}
}
実行するとCircle 3.14、Square 4.00と出力されます。ループ側はShape型しか知りません。三角形を足すときはShapeを実装したクラスを1つ追加するだけで、ループには手を入れません。継承関係のないクラス同士にも共通の振る舞いを持たせられるので、Javaでは先にこちらを検討します。インターフェースという言葉の範囲はインターフェースとは?意味と種類・コードと通信の契約を実装目線で解説で詳しく扱っています。
継承とオーバーライドで実装する
親クラスのメソッドを子クラスで上書きし、親クラス型の変数で子のインスタンスを扱います。@Overrideを付けると、メソッド名や引数を書き間違えたときにコンパイルエラーで気付けます。
class Animal {
String speak() { return "..."; }
}
class Dog extends Animal {
@Override
String speak() { return "ワン"; }
}
class Cat extends Animal {
@Override
String speak() { return "ニャー"; }
}
// 呼び出し側(mainメソッド内に置く。import java.util.List; が必要)
for (Animal x : List.of(new Dog(), new Cat())) {
System.out.println(x.speak()); // ワン、ニャー の順に出る
}
変数の型はAnimalでも、動くのはDogとCatの処理です。親クラスに共通のフィールドや処理の実装を持たせたいときに向きます。継承とポリモーフィズムは別の概念です。継承は実装を引き継ぐ仕組みで、ポリモーフィズムは同じ呼び出しで処理を切り替える性質です。継承そのものの使いどころや委譲との使い分けは継承とは?オブジェクト指向での使いどころ・委譲との使い分け・アンチパターンにまとめています。
ポリモーフィズムが効かない場面(Javaの落とし穴)
実行時の型で呼び先が決まるのは、オーバーライドされたインスタンスメソッドだけです。次のコードは、親クラス型の変数にDogを入れて4通りに呼んだものです。
class Animal {
String name = "動物";
static String kind() { return "Animal.kind"; }
String speak() { return "..."; }
}
class Dog extends Animal {
String name = "犬";
static String kind() { return "Dog.kind"; }
@Override
String speak() { return "ワン"; }
}
class Printer {
static String describe(Animal a) { return "Animal版"; }
static String describe(Dog d) { return "Dog版"; }
}
// 以下はmainメソッド内で実行
Animal a = new Dog();
System.out.println(a.speak()); // ワン
System.out.println(a.name); // 動物
System.out.println(a.kind()); // Animal.kind
System.out.println(Printer.describe(a)); // Animal版
JDK 25で実行すると、Dogの処理が動いたのはspeak()だけでした。残りの3つは宣言型のAnimalで決まっています。
フィールドとstaticメソッドの隠蔽(宣言型で決まる呼び先)
子クラスで同じ名前のフィールドやstaticメソッドを定義しても、オーバーライドにはならず、親のものを隠すだけです(隠蔽、hiding)。Java言語仕様は8.4.8.2でstaticメソッドの隠蔽を、8.3でフィールドの隠蔽を、インスタンスメソッドのオーバーライドとは別に定めています。Oracleのチュートリアル(JDK 8向け)も同じ区別を例で示しています。a.kind()はコンパイル時にAnimal.kind()へ固定されます。javacに-Xlint:allを付けると「[static] static method should be qualified by type name, Animal, instead of by an expression」と警告が出ます。staticメソッドはクラス名で呼び、フィールドはprivateにしてgetterを通すと、この取り違えは起きません。
オーバーロードの選択基準(引数の宣言型)
Printer.describe(a)は、中身がDogでもAnimal版が呼ばれました。オーバーロードのどれを呼ぶかは、コンパイル時に引数の宣言型で決まるからです。「受け取ったオブジェクトの型で処理を変えたい」なら、オーバーロードを増やすのではなく、そのオブジェクト自身のメソッドをオーバーライドします。
コンストラクタ内でのオーバーライド可能メソッドの呼び出し
親のコンストラクタが、子でオーバーライドされたメソッドを呼ぶと、子のフィールドが初期化される前に子の処理が動きます。
class Base {
Base() { init(); }
void init() {}
}
class Child extends Base {
private String label = "ready";
@Override
void init() { System.out.println("label=" + label); }
}
new Child(); // label=null と出る
出力はlabel=readyではなくlabel=nullです。Childのフィールド初期化子は、親のコンストラクタが終わってから実行されるためで、Java言語仕様 12.5の例12.5-2も同じ挙動を示しています。コンストラクタから呼ぶメソッドは、privateかfinalにしてオーバーライドできないようにするのが基本です。
JDK 25ではJEP 513(Flexible Constructor Bodies)が正式機能になり、super()より前に、初期化子を持たないフィールドへ代入できるようになりました。フィールド宣言をprivate String label;に変えて初期化子を外し、Child() { label = "ready"; super(); }と書けば、JDK 25でlabel=readyと出ることを確かめています。初期化子を残したままだとcannot assign initialized field 'label' before supertype constructor has been calledでコンパイルエラーになります。
if・switchの分岐とポリモーフィズムの使い分け
ポリモーフィズムの一番の効果は、型ごとのif・switchを呼び出し側から消せることです。「円なら」「四角なら」という分岐が複数箇所にあると、型を1つ足すたびに全箇所を直す必要があります。ポリモーフィズムなら、クラスを1つ足すだけで既存のコードに触れません。SOLID原則のオープンクローズドの原則(OCP)とは?メイヤー版とマーチン版の違いが言う「拡張に開き、修正に閉じる」を支える仕組みで、Strategyパターンとは?PHP・Java・TypeScriptの実装例と使いどころもこの性質の上に成り立っています。
ただし、どの場面にも向くわけではありません。判断の基準は「この先、何が増えるか」です。
| 今後増えるもの | 向く書き方 | 例 |
|---|---|---|
| 型(種類) | ポリモーフィズム | 決済手段・通知チャネル |
| 型に対する処理 | sealed+switch | 手数料計算・帳票出力 |
| どちらも増えない | 素直な分岐 | 2〜3種類の固定の区分 |
型の種類が固定で、型に対する処理(手数料・表示・帳票など)のほうが次々に増えるなら、処理を足すたびに全クラスへメソッドを追加することになり、ポリモーフィズムは逆に重くなります。型を足しやすい設計と処理を足しやすい設計が両立しにくいこの関係を、Philip Wadlerは1998年にExpression Problemと名付けました。Java 21以降なら、sealedで型の種類を閉じ(JEP 409、JDK 17で正式化)、switchのパターンマッチング(JEP 441、JDK 21で正式化)で処理を1か所に書けます。
sealed interface Payment permits Card, BankTransfer {}
record Card(int amount) implements Payment {}
record BankTransfer(int amount) implements Payment {}
static int fee(Payment p) {
return switch (p) {
case Card c -> c.amount() * 3 / 100;
case BankTransfer b -> 220;
};
}
fee(new Card(10000))は300、fee(new BankTransfer(10000))は220を返します。sealedで許可した型をswitchがすべて扱っていればdefaultは要りません。新しい決済手段をpermitsに足すと、扱っていないswitchがコンパイルエラーになるので、追加漏れも防げます。種類が2〜3個で今後も増えない区分なら、インターフェースを切らずにifで書いたほうが、処理を追うときにクラス間を行き来せずに済みます。
業務システムでこうした設計を決め、Javaで作り込む案件はフルスクラッチ開発として請け負っています。
Java以外の言語のポリモーフィズム(Python・C#・TypeScript・Go)
考え方はどの言語でも同じですが、何を書けばポリモーフィズムになるかが言語ごとに違います。
- Python:継承もインターフェースも要りません。Python公式の用語集が「オブジェクトの型を見ずにメソッドや属性を直接呼ぶスタイル」と定義するダックタイピングで成り立ちます。
- C#:メソッドは既定でオーバーライドできません。親で
virtual(またはabstract)、子でoverrideを付けて初めて実行時の型で呼ばれます(Microsoft Learnのポリモーフィズムの解説)。Javaと逆なので、移植のときに付け忘れが起きます。 - TypeScript:型の互換性を構造で判定する(公式ハンドブックのType Compatibility)ため、インターフェースをimplementsしていなくても、同じ形のメソッドを持つオブジェクトを渡せます。
- Go:クラスも継承もありません。インターフェースのメソッドを持つ型は、宣言しなくてもそのインターフェースを満たします(Go言語仕様)。詳しくはGoのインターフェースとは?暗黙実装とダックタイピング・型制約を実装例で解説にあります。
class Duck:
def speak(self):
return "ガーガー"
class Robot:
def speak(self):
return "ピポ"
for x in (Duck(), Robot()):
print(x.speak()) # ガーガー、ピポ の順に出る
DuckとRobotには共通の親クラスがありませんが、どちらもspeak()を持つので同じループで呼べます。
よくある質問
ポリモーフィズムや多態性・多相性の読み方は?
polymorphismは「ポリモーフィズム」と読み、「ポリモフィズム」と書かれることもあります。和訳の多態性は「たたいせい」、多相性は「たそうせい」と読みます。どれも同じ概念を指します。
継承とポリモーフィズムの違いは何ですか?
継承は、親クラスのフィールドやメソッドを子クラスが引き継ぐ仕組みです。ポリモーフィズムは、同じ呼び出しで型ごとに違う処理が動く性質です。継承はポリモーフィズムを実現する手段の1つですが、Javaではインターフェースでも実現でき、Pythonのように継承なしで成り立つ言語もあります。
オーバーロードとオーバーライドの違いは何ですか?
オーバーロードは同じ名前のメソッドを引数の型や数を変えて複数定義することで、どのシグネチャを呼ぶかはコンパイル時に引数の宣言型で決まります。オーバーライドは親のメソッドを子で上書きすることで、呼び先は実行時のオブジェクトの型で決まります。前者はアドホック多相、後者はサブタイプ多相にあたります。
ポリモーフィズムの身近な例は?
JavaのListインターフェースが身近な例です。List<String> xsにArrayListを入れてもLinkedListを入れても、xs.add("a")と同じ書き方で呼べ、内部ではそれぞれの実装が動きます。決済手段(カード・銀行振込・コード決済)ごとに支払い処理を差し替える設計も典型例です。
抽象クラスとインターフェースのどちらで実装すべきですか?
迷ったらインターフェースです。Javaのクラスは1つしか継承できませんが、インターフェースは複数実装できます。共通のフィールドや、子クラスに共通する処理の実装を親に持たせたいときだけ抽象クラスを選びます。インターフェースもJava 8以降はdefaultメソッドで実装を持てます。