ポリモーフィズムを活かす
ポリモーフィズムを活かす とは
親クラス Shape の area() を子クラス Circle と Square がそれぞれ独自に実装し、List
にまとめて合計面積を求めるコードを書きながら、ポリモーフィズムの本質である動的ディスパッ。
ポリモーフィズムは「同じ呼び方で違う動きが起こる」仕組み
継承を学ぶと、親クラスのメソッドを子クラスで @Override して書き換えられることを知ります。ここから一歩踏み込むと現れるのが ポリモーフィズム (多態性) です。日本語では難しく聞こえますが、実態は「親型の変数 に 子型のインスタンス を入れて、同じメソッドを呼ぶと、中身のクラスに応じて違う実装が動く」というだけの話です。
ポリモーフィズム (polymorphism) という言葉は、ギリシャ語の
poly(多くの) とmorph(形) を組み合わせたもので、「いろいろな形をとる」というのが直訳です。Java では同じshape.area()という 1 行が、相手によって円の面積計算にも四角形の面積計算にもなる、という挙動でこの「多くの形」が表現されます。
なぜこれが重要かというと、ポリモーフィズムが効くと「呼び出し側のコードが、相手の具体的なクラスを知らなくてよくなる」からです。たとえば描画ライブラリの中身を考えてみましょう。図形を 1 つずつ if (s instanceof Circle) ... else if (s instanceof Square) ... と分岐しながら描いていたら、新しい図形が増えるたびに if の連なりを修正する羽目になります。これは保守が大変です。一方ポリモーフィズムを使えば、for ループで s.draw() と呼ぶだけで済み、新しい図形が増えても呼び出し側はノータッチで動き続けます。
上の図のように、抽象クラス Shape を頂点に Circle Square Triangle といった具体型がぶら下がる構造を作れば、利用者は「Shape の area() を呼ぶ」とだけ意識すれば良くなります。本章ではこの考え方を、abstract クラスと List<Shape> を使った具体的なコードで体験していきます。
親型変数で子型インスタンスを参照する
まずは一番素朴な例から見てみましょう。Shape という親型の変数に、Circle という子型のインスタンスを代入するパターンです。
Java
import java.util.List;
abstract class Shape {
public abstract int area();
}
class Circle extends Shape {
private final int r;
public Circle(int r) { this.r = r; }
@Override
public int area() { return r * r; }
}
class Square extends Shape {
private final int s;
public Square(int s) { this.s = s; }
@Override
public int area() { return s * s; }
}ここでポイントなのは Shape shape = new Circle(2); のように 左側を親型、右側を子型 にして変数を宣言できるところです。これを アップキャスト と呼びます。逆向きはできず、暗黙の String のような変換も起こりません。アップキャストは Java が暗黙にやってくれるので、(Shape) のような明示的なキャストすら要りません。
Shapeはabstract classなので、new Shape()自体は禁止されています。abstractメソッドを含むクラスは「設計図の途中」のような状態で、誰かが継承してarea()の中身を埋めてくれて初めて使い物になる、というのが Java の決まりです。
このとき shape.area() を呼ぶと、変数の型は Shape ですが、実際に動くのは Circle の area() です。なぜそうなるかというと、Java は 実行時にオブジェクトの本当のクラスを見て、どのメソッドを呼ぶかを決める からです。これを 動的ディスパッチ (dynamic dispatch / 動的束縛) と呼びます。
List<Shape> でまとめて扱うのが定番パターン
ポリモーフィズムが最も威力を発揮するのは、List<Shape> のように親型のコレクションへ複数の子型を放り込んで、ループで一括処理するときです。
Java
List<Shape> shapes = List.of(new Circle(2), new Square(3));
int total = 0;
for (Shape s : shapes) {
total += s.area();
}Circle と Square は別のクラスですが、どちらも Shape の子なので 1 つの List<Shape> にまとめられます。for-each で取り出した s は型としては Shape ですが、s.area() を呼べば実体に応じて Circle.area() か Square.area() のどちらかが動きます。呼び出し側 (for ループ) は中身の型を 1 つも気にしていません。これがポリモーフィズムの真骨頂です。
仮にあとから Triangle Pentagon Star などの図形が追加されても、shapes.add(new Triangle(...)); するだけで合計計算のロジックは 1 文字も変えずに動きます。「拡張に対して開いて (open)、修正に対して閉じている (closed)」という OCP (Open Closed Principle) の代表例として、ポリモーフィズムは設計の教科書で繰り返し登場します。
よく似たデザインパターンへの架け橋
この「親型で受け取って中身に応じた振る舞いをさせる」考え方は、いくつかのデザインパターンの土台になります。今は名前だけ眺めておいて、後の章でじっくり扱う予定です。
ストラテジー (Strategy) パターン— 「価格計算ロジック」をPriceStrategyインターフェースで抽象化し、RegularStrategyMemberStrategySaleStrategyなどを差し替える。決済システムの割引切り替えなどで頻出テンプレートメソッド (Template Method) パターン— 親クラスに処理の骨組みを書き、子クラスで一部のステップだけを@Overrideする。SpringのJdbcTemplateやAbstractListでも内部的に使われている考え方ファクトリ (Factory) パターン— 利用者には親型だけを返し、実体の生成は別のクラスに任せる。Calendar.getInstance()のような API はこの典型例
どのパターンも「親型 / インターフェースを使って実装を切り離す」という共通項を持っています。ポリモーフィズムが手に馴染んでいないと、これらのパターンも「なぜ嬉しいのか」が分からなくなりがちです。逆に言えば、ポリモーフィズムを腹落ちさせれば、デザインパターンの 8 割は同じ景色に見えてきます。
よくある間違い
ポリモーフィズムは仕組み自体は素直ですが、初学者が踏みがちな落とし穴がいくつかあります。
- キャストが必要だと思い込む —
Shape s = new Circle(2);のあとs.area();を呼ぶのに、((Circle) s).area();のように書く必要はありません。子型に固有のメソッドを呼びたいときだけダウンキャストが必要です。共通メソッドを呼ぶだけなら親型のままで十分です staticメソッドはポリモーフィズムの対象外 —staticメソッドはクラスに紐づくため、@Overrideではなく隠蔽(hiding) されるだけです。Shape s = new Circle(2); s.staticMethod();と書いても、呼ばれるのは変数型Shapeのstaticメソッドです。インスタンスメソッドだけが動的ディスパッチの対象だと覚えてくださいfinalメソッドはオーバーライド禁止 — 親クラスのメソッドにfinalが付いていると、子クラスで@Overrideしようとした瞬間にコンパイルエラーになります。同様にfinal class自体は継承できません。「これ以上拡張させたくない」という意思表示の機能なので、ライブラリ作者が意図的に付けることが多いです
もう 1 つ細かいですが厄介なのが、@Override アノテーションを付け忘れて、メソッド名や引数の型をうっかり間違えてしまうケースです。@Override を付けておくと、シグネチャがずれていればコンパイラが教えてくれるので、必ず付ける習慣にしておきましょう。
IDEの警告レベルを「@Override漏れを warning 以上」にしておくのも安全策です。IntelliJ IDEAもEclipseもデフォルトで警告してくれますが、たまにオフにしている設定の人がいるので確認しておくと良いです。
やってみよう
それでは今回の課題に挑戦しましょう。やることは次のとおりです。
Shapeをabstract classとして宣言し、抽象メソッドpublic abstract int area();を持たせるCircle extends Shapeとして、r * rを返すarea()を実装する (本当の円ではなく整数で簡略化)Square extends Shapeとして、s * sを返すarea()を実装するSolution.totalArea(int circleR, int squareS)の中でList<Shape>を作り、new Circle(circleR)とnew Square(squareS)を入れるfor-eachで全要素のarea()を合計し、intで返す
テストでは totalArea(2, 3) → 13 だけでなく、totalArea(5, 1) → 26 や totalArea(1, 0) → 1 のような別パターンでも呼ばれます。13 を直接 return するような「定数合わせ」では通らないので、ちゃんと引数から Circle Square を生成し、ポリモーフィズム経由で area() を呼んで合計してください。
コードを書き終わったら、3 つ目の図形 (例えば Triangle クラスを足して base * height / 2) を追加してみたり、リストへの追加順を入れ替えたりして、for ループが「中身が何であっても area() を呼ぶだけ」で済んでいる気持ち良さを味わってみてください。次のレッスンでは、abstract class のかわりに interface を使ったポリモーフィズムや、Comparable<T> のような標準的なインターフェース活用を見ていきます。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は 継承とポリモーフィズム まとめクイズ で、継承とポリモーフィズム まとめクイズ を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- ポリモーフィズム の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. ポリモーフィズム とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
Shapeをabstract classとして宣言し、抽象メソッドpublic abstract int area();を持たせることCircle extends Shapeでr * rを返すarea()、Square extends Shapeでs * sを返すarea()を実装することSolution.totalArea(int circleR, int squareS)の中でList<Shape>にnew Circle(circleR)とnew Square(squareS)を入れ、for-eachでarea()を合計してintで返すこと
入出力例
test-cases.txt
totalArea(2, 3) → 13
totalArea(5, 1) → 26
totalArea(1, 0) → 1