- JAVA
Javaポリモーフィズムとは?多態性の例とアップキャスト・ダウンキャスト
ポリモーフィズム(多態性)とは、同じメソッド呼び出しが、変数に入っているオブジェクトの種類によって違う動きをする仕組みです。 Javaでは「親の型の変数に子のインスタンスを入れる(アップキャスト)」と「子が親のメソッドを上書きする(オーバーライド)」の2つを組み合わせて使います。呼び出す側は a.cry() と1行書くだけで、実体が犬なら「ワンワン」、猫なら「ニャー」と動きが切り替わります。
この記事では、ポリモーフィズムを使わない版と使う版を並べて実行結果を比較し、アップキャスト・ダウンキャストの違い、ClassCastException が出る流れとスタックトレース、instanceof とパターンマッチングによる書き直しまでを順に確かめます。掲載したコードはすべて OpenJDK 21.0.10 で実行し、出力をそのまま載せています。ファイル名を合わせて java ファイル名.java で動かせます。
ポリモーフィズムの早見表
| 知りたいこと | 答え |
|---|---|
| ポリモーフィズムとは | 同じ呼び出しが、実体の型に応じて違う動きをする仕組み |
| 成り立つ条件 | (1) 継承または実装の関係がある (2) メソッドをオーバーライドしている (3) 親の型の変数で子のインスタンスを扱う |
| どのメソッドが動くか | 実体の型(newした型) のメソッドが実行時に選ばれる |
| 呼べるメソッドの範囲 | 変数の型 に宣言されたメソッドだけ(コンパイル時に決まる) |
| アップキャスト | 子 → 親の型へ。キャストの記述は不要で、常に成功する |
| ダウンキャスト | 親 → 子の型へ。(Dog) a の記述が必要。実体が合わないと ClassCastException |
| 安全なダウンキャスト | if (a instanceof Dog d) { d.fetch(); }(Java 16以降) |
| 多態になるもの | オーバーライドしたインスタンスメソッド、インタフェースのdefaultメソッド |
| 多態にならないもの | フィールド、staticメソッド、privateメソッド、オーバーロードの選択 |
| 主な使いどころ | 引数や戻り値、コレクションの型を親やインタフェースにして、実体を入れ替えられるようにする |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaポリモーフィズムとは?多態性の例とアップキャスト・ダウンキャスト」
ポリモーフィズムを最小のコードで確かめる
まずは最小の例です。Animal を継承した Dog を作り、Animal 型の変数に入れて呼び出します。
// Basic.java
public class Basic {
static class Animal {
void cry() { System.out.println("(何かが鳴いた)"); }
}
static class Dog extends Animal {
@Override void cry() { System.out.println("ワンワン"); }
void fetch() { System.out.println("ボールを取ってきた"); }
}
public static void main(String[] args) {
Animal a = new Dog(); // 変数の型はAnimal、実体はDog
a.cry(); // Dogのcry()が動く
System.out.println(a.getClass().getSimpleName());
System.out.println(a instanceof Animal);
System.out.println(a instanceof Dog);
}
}
実行結果(java Basic.java):
ワンワン
Dog
true
true
変数の型は Animal ですが、動いたのは Dog の cry() です。どのメソッドが動くかは、変数の型ではなく実体の型で決まります。 この実行時の選択を「動的ディスパッチ(動的束縛)」と呼びます。
変数の型と実体の型を分けて考える
ポリモーフィズムを理解する近道は、1つの変数に「2つの型」があると捉えることです。
| 見るもの | 例 Animal a = new Dog(); |
決まるタイミング | 何を決めるか |
|---|---|---|---|
| 変数の型(静的な型) | Animal |
コンパイル時 | 呼べるメソッド・見えるフィールドの範囲 |
| 実体の型(動的な型) | Dog |
実行時 | オーバーライドされたメソッドのうち、どれが動くか |
変数の型に無いメソッドは、実体が持っていても呼べません。上の a で a.fetch() と書くと、次のコンパイルエラーになります。
CompileErr.java:8: error: cannot find symbol
a.fetch();
^
symbol: method fetch()
location: variable a of type Animal
1 error
error: compilation failed
コンパイラは変数の型 Animal だけを見て判断するため、Animal に fetch() が無ければ受け付けません。fetch() を呼びたいときは、後で説明するダウンキャストを使います。
使わない版と使う版を実行結果で比べる
ポリモーフィズムの価値は、同じ処理を2通りに書くとはっきりします。3種類の動物を順に鳴かせるプログラムで比べます。
使わない版:型ごとに分岐する
共通の親を持たないクラスを Object の配列に入れ、型を調べて呼び分けます。
// NoPoly.java
public class NoPoly {
static class Dog { void bark() { System.out.println("ワンワン"); } }
static class Cat { void meow() { System.out.println("ニャー"); } }
static class Bird { void sing() { System.out.println("ピヨピヨ"); } }
public static void main(String[] args) {
Object[] animals = { new Dog(), new Cat(), new Bird() };
for (Object a : animals) {
// 型ごとに分岐し、型ごとに別のメソッドを呼ぶ
if (a instanceof Dog) {
((Dog) a).bark();
} else if (a instanceof Cat) {
((Cat) a).meow();
} else if (a instanceof Bird) {
((Bird) a).sing();
}
}
}
}
実行結果:
ワンワン
ニャー
ピヨピヨ
使う版:呼び出しは1行になる
共通の親 Animal に cry() を用意し、各クラスがオーバーライドします。
// WithPoly.java
public class WithPoly {
static abstract class Animal {
abstract void cry(); // 鳴き方はサブクラスが決める
}
static class Dog extends Animal {
@Override void cry() { System.out.println("ワンワン"); }
}
static class Cat extends Animal {
@Override void cry() { System.out.println("ニャー"); }
}
static class Bird extends Animal {
@Override void cry() { System.out.println("ピヨピヨ"); }
}
public static void main(String[] args) {
Animal[] animals = { new Dog(), new Cat(), new Bird() };
for (Animal a : animals) {
a.cry(); // 呼び出しは1行。動きは実体の型で決まる
}
}
}
実行結果:
ワンワン
ニャー
ピヨピヨ
出力はまったく同じです。違いは、呼び出す側のコードの形に表れます。
2つの版の違いを表で見る
| 比べる点 | 使わない版 | 使う版 |
|---|---|---|
| 呼び出し側の行数 | 種類の数だけ if が並ぶ |
a.cry() の1行 |
| 種類を1つ増やすとき | クラスの追加に加え、分岐のある場所をすべて直す | クラスを1つ追加するだけ |
| 分岐の書き漏れ | 実行するまで気づきにくい(何も出力されない) | 抽象メソッドの実装漏れはコンパイルエラーで分かる |
| メソッド名 | bark meow sing とばらばら |
cry に統一 |
| 呼び出し側が知る型 | Dog Cat Bird すべて |
Animal だけ |
種類が増えても呼び出し側を直さずに済むことが、ポリモーフィズムのいちばんの利点です。「新しい種類を足すときに、既存のコードを書き換える箇所が少ない」設計になります。
クラスの関係を図で確認する
classDiagram
class Animal {
<<abstract>>
+cry()
}
class Dog {
+cry()
+fetch()
}
class Cat {
+cry()
+scratch()
}
Animal <|-- Dog
Animal <|-- Cat
矢印の先(Animal)が親、根元(Dog Cat)が子です。子から親の方向へ型を変えるのがアップキャスト、親から子の方向がダウンキャストです。
アップキャストとダウンキャストの違い
違いをまとめた比較表
| 項目 | アップキャスト | ダウンキャスト |
|---|---|---|
| 変換の向き | 子の型 → 親の型 | 親の型 → 子の型 |
| 書き方 | Animal a = new Dog(); |
Dog d = (Dog) a; |
| キャストの記述 | 不要(自動で行われる) | 必要((型) を書く) |
| コンパイル時の確認 | 継承関係があれば通る | 継承関係があれば通る(実体までは見ない) |
| 実行時の確認 | なし。常に成功する | あり。実体が合わないと ClassCastException |
| 呼べるメソッド | 親の型にあるものだけに絞られる | 子の型に固有のメソッドも呼べるようになる |
| オブジェクトそのもの | 変わらない | 変わらない |
| 主な目的 | 複数の種類をまとめて同じ形で扱う | 子に固有の機能を使う |
| 事前の確認 | 不要 | instanceof で確かめてから行う |
どちらのキャストもオブジェクト自体は変化しません。変わるのは「どの型の窓からそのオブジェクトを見るか」だけです。アップキャストすると窓が狭くなり、ダウンキャストすると元の広さに戻る、と考えると整理しやすくなります。
アップキャストが起きる4つの場面
アップキャストは代入のときだけでなく、いろいろな場面で自動的に行われています。
| 場面 | コード例 |
|---|---|
| 変数への代入 | Animal a = new Dog(); |
| メソッドの引数 | void play(Animal a) に play(new Dog()) と渡す |
| メソッドの戻り値 | 戻り値の型が Animal のメソッドで return new Dog(); |
| 配列・コレクション | List<Animal> list = List.of(new Dog(), new Cat()); |
ダウンキャストでClassCastExceptionを再現する
ダウンキャストは、実体が指定した型(またはその子)であるときだけ成功します。成功する場合と、例外になる場合を続けて実行します。
// CastDemo.java
public class CastDemo {
static class Animal { void cry() { System.out.println("(何かが鳴いた)"); } }
static class Dog extends Animal {
@Override void cry() { System.out.println("ワンワン"); }
void fetch() { System.out.println("ボールを取ってきた"); }
}
static class Cat extends Animal {
@Override void cry() { System.out.println("ニャー"); }
void scratch() { System.out.println("爪をといだ"); }
}
public static void main(String[] args) {
Animal a = new Dog(); // アップキャスト(キャストの記述は不要)
Dog d = (Dog) a; // ダウンキャスト(実体がDogなので成功)
d.fetch();
Animal b = new Cat();
Dog e = (Dog) b; // 実体はCat。コンパイルは通るが実行時に例外
e.fetch();
}
}
実行結果(javac CastDemo.java のあと java CastDemo):
ボールを取ってきた
Exception in thread "main" java.lang.ClassCastException: class CastDemo$Cat cannot be cast to class CastDemo$Dog (CastDemo$Cat and CastDemo$Dog are in unnamed module of loader 'app')
at CastDemo.main(CastDemo.java:18)
1つ目のダウンキャストは成功し、2つ目で ClassCastException が発生しました。コンパイルは通るのに実行時に止まる点が、ダウンキャストで気をつけたいところです。
スタックトレースの読み方
| 表示されている部分 | 意味 |
|---|---|
java.lang.ClassCastException |
型変換が成立しなかったことを表す例外 |
class CastDemo$Cat cannot be cast to class CastDemo$Dog |
実体は Cat で、Dog には変換できないという内容。$ は入れ子クラスを表す |
are in unnamed module of loader 'app' |
両方のクラスが属するモジュールとクラスローダーの情報 |
at CastDemo.main(CastDemo.java:18) |
発生場所。18行目の Dog e = (Dog) b; |
メッセージの「A cannot be cast to B」を「実体はA、変換しようとした型はB」と読むと、原因がすぐ分かります。なお java CastDemo.java のようにソースファイルを直接実行した場合、括弧内のローダー名は com.sun.tools.javac.launcher.Main$MemoryClassLoader @19dc67c2 のような表示になりました(例外の種類と行番号は同じです)。
継承関係がない型へのキャストはコンパイルエラーになる
Dog と String のように親子関係がまったくないクラス同士では、キャストを書いた時点でコンパイラが「変換できない型」として知らせてくれます(この1点は今回未実行で、Java言語の一般に知られた仕様としての説明です)。実行時の例外になるのは、コンパイラから見て「成立する可能性がある」キャストです。Animal 型の変数の中身は Dog かもしれないし Cat かもしれないので、コンパイラは (Dog) b を通します。
instanceofとパターンマッチングで安全に書き直す
ClassCastException を避ける基本は、キャストの前に instanceof で実体の型を確かめることです。Javaのバージョンによって3通りの書き方があります。
// SafeCast.java
import java.util.List;
public class SafeCast {
static class Animal { void cry() { System.out.println("(何かが鳴いた)"); } }
static class Dog extends Animal {
@Override void cry() { System.out.println("ワンワン"); }
void fetch() { System.out.println("ボールを取ってきた"); }
}
static class Cat extends Animal {
@Override void cry() { System.out.println("ニャー"); }
void scratch() { System.out.println("爪をといだ"); }
}
public static void main(String[] args) {
List<Animal> animals = List.of(new Dog(), new Cat());
System.out.println("--- 1. instanceof + キャスト(どのバージョンでも書ける)");
for (Animal a : animals) {
if (a instanceof Dog) {
Dog d = (Dog) a;
d.fetch();
} else if (a instanceof Cat) {
Cat c = (Cat) a;
c.scratch();
}
}
System.out.println("--- 2. instanceofのパターンマッチング(Java 16以降)");
for (Animal a : animals) {
if (a instanceof Dog d) {
d.fetch();
} else if (a instanceof Cat c) {
c.scratch();
}
}
System.out.println("--- 3. switchのパターンマッチング(Java 21以降)");
for (Animal a : animals) {
switch (a) {
case Dog d -> d.fetch();
case Cat c -> c.scratch();
default -> a.cry();
}
}
System.out.println("--- 4. nullはinstanceofでfalseになる");
Animal n = null;
System.out.println(n instanceof Dog);
}
}
実行結果:
--- 1. instanceof + キャスト(どのバージョンでも書ける)
ボールを取ってきた
爪をといだ
--- 2. instanceofのパターンマッチング(Java 16以降)
ボールを取ってきた
爪をといだ
--- 3. switchのパターンマッチング(Java 21以降)
ボールを取ってきた
爪をといだ
--- 4. nullはinstanceofでfalseになる
false
3つの書き方の比較表
| 書き方 | 使えるバージョン | 特徴 | 向いている場面 |
|---|---|---|---|
instanceof + キャスト |
すべて | 判定とキャストで型名を2回書く | Java 8〜11 のコードに合わせるとき |
instanceof Dog d |
Java 16以降 | 判定・キャスト・変数宣言が1つにまとまる | 1〜2種類を判定するとき |
switch のパターン |
Java 21以降 | 種類ごとの処理を並べて書ける | 3種類以上を振り分けるとき |
パターンマッチングでは、判定と変数への代入が同時に行われるため、型名の書き間違いによる ClassCastException が起きる余地がなくなります。また、null に対する instanceof は常に false になるので、nullチェックを別に書かなくても if の中には入りません。
sealedと組み合わせて網羅を確認する
Java 17以降の sealed(継承できる型を限定する機能)と組み合わせると、switch で全種類を扱えているかをコンパイラが確認してくれます。
// Sealed.java
public class Sealed {
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
static double area(Shape s) {
return switch (s) { // defaultなしで全種類を網羅できる
case Circle c -> Math.PI * c.r() * c.r();
case Rect r -> r.w() * r.h();
};
}
public static void main(String[] args) {
System.out.printf("%.2f%n", area(new Circle(1)));
System.out.printf("%.2f%n", area(new Rect(2, 3)));
}
}
実行結果:
3.14
6.00
ダウンキャストが必要になったら設計を見直す合図
ダウンキャストや型判定が何度も出てくるときは、その処理を親の型のメソッドとして定義し、各クラスでオーバーライドできないかを考えてみると、コードがすっきりすることが多くあります。
| 状況 | おすすめの書き方 |
|---|---|
| すべての種類に共通する操作 | 親にメソッドを定義してオーバーライドする(型判定は不要) |
| 一部の種類だけが持つ操作 | instanceof のパターンマッチングで確かめてから呼ぶ |
| 種類が固定で、種類ごとに計算が違う | sealed + switch のパターンで網羅する |
| 外部のライブラリの型で、メソッドを足せない | instanceof または switch で振り分ける |
多態にならないもの:フィールド・static・private
「実体の型で決まる」のは、オーバーライドしたインスタンスメソッドだけです。フィールド、staticメソッド、privateメソッドは変数の型(または定義したクラス)で決まります。1つのプログラムでまとめて確かめます。
// FieldStatic.java
public class FieldStatic {
static class Parent {
String name = "Parentのフィールド";
static void hello() { System.out.println("Parent.hello()"); }
void show() { System.out.println("Parent.show()"); }
private void secret() { System.out.println("Parent.secret()"); }
void callSecret() { secret(); }
}
static class Child extends Parent {
String name = "Childのフィールド"; // フィールドの隠蔽
static void hello() { System.out.println("Child.hello()"); } // staticメソッドの隠蔽
@Override void show() { System.out.println("Child.show()"); } // オーバーライド
void secret() { System.out.println("Child.secret()"); } // 別メソッド扱い
}
public static void main(String[] args) {
Parent p = new Child();
p.show(); // インスタンスメソッド → 実体の型
System.out.println(p.name); // フィールド → 変数の型
p.hello(); // staticメソッド → 変数の型
p.callSecret(); // privateメソッド → 定義したクラス
System.out.println(((Child) p).name); // ダウンキャストすればChild側が見える
}
}
実行結果:
Child.show()
Parentのフィールド
Parent.hello()
Parent.secret()
Childのフィールド
何が実体の型で決まるかの一覧
| 対象 | 決まり方 | 上の結果 | 呼び名 |
|---|---|---|---|
| インスタンスメソッド | 実体の型(実行時) | Child.show() |
オーバーライド |
| フィールド | 変数の型(コンパイル時) | Parentのフィールド |
隠蔽(hiding) |
| staticメソッド | 変数の型(コンパイル時) | Parent.hello() |
隠蔽(hiding) |
| privateメソッド | 定義したクラス | Parent.secret() |
継承されないので別メソッド |
| コンストラクタ | new に書いた型 |
― | 継承されない |
この結果から、次の3つの書き方がおすすめです。
- フィールドは
privateにして、値の取得はメソッド(getter)経由にする。メソッドなら多態が働きます。 - staticメソッドは
Parent.hello()のようにクラス名で呼ぶ。変数経由で呼ぶと、どちらが動くかが読み手に伝わりにくくなります。 - 親と同じ名前のフィールドを子で定義しない。必要な値は親のフィールドを使うか、別の名前にします。
@Overrideで上書きの書き間違いを見つける
メソッド名や引数の型が少しでも違うと、オーバーライドではなく「別のメソッドの追加」になり、多態が働きません。@Override を付けておくと、親に同じシグネチャ(メソッド名と引数の並び)のメソッドが無い場合にコンパイラが知らせてくれます。オーバーライドするメソッドには必ず @Override を付ける習慣にすると安心です。
オーバーライドが成立する主な条件は次のとおりです(Java言語仕様の一般的な規則です)。
| 条件 | 内容 |
|---|---|
| メソッド名と引数 | 親と同じ名前、同じ引数の型と並び |
| 戻り値の型 | 親と同じ、または親の戻り値の子の型(共変戻り値) |
| アクセス修飾子 | 親と同じか、より広い範囲(protected → public は可) |
| 例外 | 親が宣言した検査例外の範囲を広げない |
| 対象外 | final・static・private のメソッドはオーバーライドできない |
オーバーライドとオーバーロードの違い
名前が似ている2つですが、メソッドが選ばれるタイミングが違います。オーバーロード(同じ名前で引数が違うメソッドを複数定義すること)は、コンパイル時に変数の型で選ばれます。
// Overload.java
public class Overload {
static class Animal {}
static class Dog extends Animal {}
static void greet(Animal a) { System.out.println("greet(Animal)"); }
static void greet(Dog d) { System.out.println("greet(Dog)"); }
public static void main(String[] args) {
Animal a = new Dog();
Dog d = new Dog();
greet(a); // 変数の型Animalで決まる
greet(d); // 変数の型Dogで決まる
greet((Dog) a); // キャストすると変数側の型がDogになる
}
}
実行結果:
greet(Animal)
greet(Dog)
greet(Dog)
1行目に注目してください。a の実体は Dog ですが、呼ばれたのは greet(Animal) です。引数として渡したオブジェクトの実体の型は、オーバーロードの選択には使われません。
| 項目 | オーバーライド | オーバーロード |
|---|---|---|
| 意味 | 親のメソッドを子で上書きする | 同じ名前で引数が違うメソッドを複数用意する |
| 関係するクラス | 親と子 | 同じクラス内(継承先でも可) |
| 引数 | 親と同じ | 型・個数・並びのどれかが違う |
| 選ばれるタイミング | 実行時(実体の型) | コンパイル時(変数の型) |
| 多態性の種類 | 動的な多態(この記事の主題) | 静的な多態と呼ばれることがある |
| 確認の手段 | @Override |
専用のアノテーションはない |
引数の実体の型に応じて処理を変えたいときは、オーバーロードではなく、引数のオブジェクト自身にメソッドを持たせてオーバーライドするか、switch のパターンマッチングで振り分けます。
インタフェースでポリモーフィズムを使う
継承(extends)だけでなく、インタフェースの実装(implements)でも同じように多態が働きます。通知の送り先を入れ替える例です。
// Iface.java
import java.util.List;
public class Iface {
interface Notifier {
void send(String message);
default void sendAll(List<String> messages) { // defaultメソッドも多態に参加する
for (String m : messages) send(m);
}
}
static class MailNotifier implements Notifier {
@Override public void send(String message) { System.out.println("[メール] " + message); }
}
static class ChatNotifier implements Notifier {
@Override public void send(String message) { System.out.println("[チャット] " + message); }
}
// 引数の型をインタフェースにすると、渡す実体を入れ替えられる
static void notifyStock(Notifier notifier) {
notifier.send("在庫が残り3個です");
}
public static void main(String[] args) {
notifyStock(new MailNotifier());
notifyStock(new ChatNotifier());
notifyStock(m -> System.out.println("[ラムダ] " + m)); // ラムダ式も実体になれる
new ChatNotifier().sendAll(List.of("A", "B"));
}
}
実行結果:
[メール] 在庫が残り3個です
[チャット] 在庫が残り3個です
[ラムダ] 在庫が残り3個です
[チャット] A
[チャット] B
notifyStock は Notifier という型しか知りません。それでも、渡す実体を変えるだけで送り先が切り替わります。抽象メソッドが1つだけのインタフェースなら、ラムダ式も実体として渡せます。default メソッドの sendAll の中で呼ばれる send も、実体(ChatNotifier)のものが選ばれています。
標準ライブラリで使われている例
ふだん書いているコードにも、ポリモーフィズムはたくさん登場しています。
| 書き方 | 変数の型 | 実体の型 | 得られること |
|---|---|---|---|
List<String> list = new ArrayList<>(); |
List |
ArrayList |
あとで LinkedList などに替えても、使う側のコードはそのまま |
Map<String, Integer> map = new HashMap<>(); |
Map |
HashMap |
順序が必要になったら LinkedHashMap や TreeMap に差し替えられる |
Runnable task = () -> System.out.println("run"); |
Runnable |
ラムダ式 | 処理そのものを引数として渡せる |
System.out.println(obj); |
Object |
任意のクラス | 各クラスがオーバーライドした toString() が呼ばれる |
Comparator<String> c = Comparator.naturalOrder(); |
Comparator |
実装クラス | 並べ替えの規則を入れ替えられる |
println(obj) が toString() の内容を表示するのも、Object 型で受け取って実体の toString() を呼ぶ、というポリモーフィズムの働きです。
継承・抽象クラス・インタフェースの使い分け早見表
ポリモーフィズムを使う土台は3種類あります。どれを選ぶかの目安を表にまとめます。
| 比べる点 | 通常クラスの継承 | 抽象クラス | インタフェース |
|---|---|---|---|
| キーワード | extends |
abstract class + extends |
interface + implements |
| 親だけでインスタンス化 | できる | できない | できない |
| 抽象メソッド | 持てない | 持てる | 持てる(基本はすべて抽象) |
| 処理を持つメソッド | 持てる | 持てる | default・static・private メソッドで持てる |
| インスタンスフィールド | 持てる | 持てる | 持てない(定数のみ) |
| コンストラクタ | 持てる | 持てる | 持てない |
| 1つのクラスが使える数 | 1つだけ | 1つだけ | 複数 |
| 表す関係 | 「〜は〜の一種」 | 「〜は〜の一種」で共通処理を持つ | 「〜ができる」という役割 |
選び方の目安
| やりたいこと | 選ぶもの |
|---|---|
| 「できること」だけを決めて、実装は各クラスに任せたい | インタフェース |
| 関係のないクラス同士に同じ役割を持たせたい | インタフェース |
| 共通のフィールドや処理を親にまとめ、一部だけ子に書かせたい | 抽象クラス |
| 親の実装漏れをコンパイル時に見つけたい | 抽象クラスまたはインタフェース(抽象メソッド) |
| すでに動いているクラスの一部の動きだけを変えたい | 通常クラスの継承 |
種類の一覧を固定し、switch で網羅したい |
sealed インタフェース + record |
迷ったときは、まずインタフェースで型を決め、共通処理が増えてきたら抽象クラスを間に挟む、という順で考えると無理のない構成になります。
つまずきやすいポイントと書き方のコツ
コンストラクタからオーバーライドされるメソッドを呼ぶとnullが見える
コンストラクタの中でも動的ディスパッチは働きます。そのため、親のコンストラクタが呼んだメソッドが子でオーバーライドされていると、子のフィールドが初期化される前に子のメソッドが動きます。
// CtorTrap.java
public class CtorTrap {
static class Base {
Base() { init(); } // コンストラクタからオーバーライド可能なメソッドを呼ぶ
void init() { System.out.println("Base.init()"); }
}
static class Sub extends Base {
private String label = "準備完了";
@Override void init() { System.out.println("Sub.init() label=" + label); }
}
public static void main(String[] args) {
new Sub();
}
}
実行結果:
Sub.init() label=null
label には「準備完了」を代入しているのに、表示は null です。親のコンストラクタ → 子のフィールド初期化、の順で処理が進むためです。コンストラクタから呼ぶメソッドは private か final にすると、この順序の影響を受けなくなります。
配列のアップキャストは実行時に型が確認される
配列も Object[] objs = new String[2]; のようにアップキャストできます。ただし実体は String[] のままなので、別の型の要素を入れると実行時に ArrayStoreException になります。
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
at ArrayStore.main(ArrayStore.java:5)
(objs[1] = Integer.valueOf(1); を実行したときの出力です。)
ジェネリクスのListは要素の親子関係を引き継がない
Dog は Animal の子ですが、List<Dog> は List<Animal> の子ではありません。代入するとコンパイルエラーになります。
Generic.java:7: error: incompatible types: List<Dog> cannot be converted to List<Animal>
List<Animal> animals = dogs;
^
1 error
error: compilation failed
読み取り専用で受け取りたいときは、引数の型を List<? extends Animal> にすると、List<Dog> も List<Cat> も渡せるようになります。
よく見るエラーと対処の一覧
| 表示・症状 | 原因 | 対処 |
|---|---|---|
cannot find symbol ... method fetch() |
変数の型に無いメソッドを呼んでいる | 親にメソッドを定義するか、instanceof Dog d で確かめてから呼ぶ |
ClassCastException: A cannot be cast to B |
実体がAなのにBへダウンキャストした | instanceof で判定してからキャストする |
| オーバーライドしたはずの処理が動かない | 引数の型や名前が違い、別メソッドになっている | @Override を付けてコンパイラに確認してもらう |
| 子で定義したフィールドの値が出ない | フィールドは変数の型で決まる | フィールドを private にしてgetterをオーバーライドする |
| staticメソッドが親の方で動く | staticメソッドは多態にならない | クラス名で呼ぶ。切り替えたい処理はインスタンスメソッドにする |
コンストラクタ内で null が見える |
子のフィールド初期化前に子のメソッドが動いた | コンストラクタから呼ぶメソッドを private か final にする |
List<Dog> cannot be converted to List<Animal> |
ジェネリクスは型引数の親子関係を引き継がない | List<? extends Animal> で受け取る |
書くときのチェックリスト
| 確認すること | 目安 |
|---|---|
| 変数・引数・戻り値の型 | できるだけ親の型やインタフェースで宣言する |
| オーバーライド | すべてに @Override を付ける |
| ダウンキャスト | instanceof のパターンマッチングとセットで書く |
種類ごとの if の列 |
親のメソッドにまとめられないか考える |
| フィールド | private にしてメソッド経由で公開する |
| staticメソッド | クラス名で呼ぶ |
よくある質問
Q. ポリモーフィズムと継承は何が違いますか? A. 継承は「親のフィールドやメソッドを子が引き継ぐ仕組み」、ポリモーフィズムは「同じ呼び出しで実体ごとに動きが変わる性質」です。継承(またはインタフェースの実装)とオーバーライドがあって、はじめてポリモーフィズムが成り立ちます。継承は土台、ポリモーフィズムはその土台の活かし方、という関係です。
Q. アップキャストするとサブクラスの情報は消えますか?
A. 消えません。オブジェクトは new したときの型のままで、変数の型という「見え方」だけが変わります。そのため a.getClass() は実体の型を返し、ダウンキャストすれば子に固有のメソッドを再び呼べます。
Q. ダウンキャストはなるべく使わないほうがよいですか?
A. 必要な場面はあります。子に固有の機能を使うときや、equals の実装で相手の型を確かめるときなどです。使うときは instanceof のパターンマッチングと組み合わせると安全に書けます。種類ごとの分岐が何度も出てくる場合は、親にメソッドを定義してオーバーライドする形にできないかを検討するとよいでしょう。
Q. 抽象クラスやインタフェースが無くてもポリモーフィズムは使えますか? A. 使えます。通常のクラスを継承してメソッドをオーバーライドすれば多態が働きます(この記事の最初の例がそうです)。抽象クラスやインタフェースを使うと、「子が必ず実装する」ことをコンパイラが確認してくれるため、実装漏れに早く気づけます。
まとめ
- ポリモーフィズムは、同じ呼び出しが実体の型に応じて違う動きをする仕組み。継承または実装、オーバーライド、親の型の変数の3つがそろうと働く
- 呼べるメソッドは変数の型で、動くメソッドは実体の型で決まる
- アップキャストは自動で常に成功、ダウンキャストは記述が必要で実行時に確認される
- 実体が合わないダウンキャストは
ClassCastException。メッセージは「実体の型 cannot be cast to 変換先の型」と読む instanceof Dog d(Java 16以降)やswitchのパターン(Java 21以降) を使うと、判定とキャストを1つにまとめられる- フィールド・staticメソッド・privateメソッド・オーバーロードの選択は多態にならない
- 迷ったら インタフェースで型を決め、共通処理が増えたら抽象クラス を検討する
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaポリモーフィズムとは?多態性の例とアップキャスト・ダウンキャスト」
株式会社RJCでは、Javaのポリモーフィズムやオブジェクト指向の考え方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。