extends で継承する
このレッスンで分かること
class Dog extends Animalで既存クラスの機能を引き継ぐextendsできるのは 1 つの親クラスだけ (多重継承は不可)。複数の機能はinterfaceで取り込むprivateフィールドは継承しても子から直接触れない。protectedか getter 経由でアクセス- 全クラスは暗黙に
Objectを継承していて、toStringequalshashCodeを持つ
extends で継承する とは
Java の
extendsキーワードを使った継承の基本を学び、Animalを継承したDogクラスを作ってDog.bark()の結果を返すコードを書いてみよう。
既存クラスを土台にする「継承」
オブジェクト指向プログラミングの三本柱は カプセル化 継承 ポリモーフィズム の 3 つだとよく言われます。本レッスンではその真ん中、継承 (inheritance) を扱います。継承とは一言でいえば、「すでにあるクラスの機能をまるごと引き継いで、それに自分だけの追加機能を載せた新しいクラスを作る」仕組みです。
たとえば動物を表す Animal クラスに eat() や sleep() といった共通の振る舞いを書いておけば、犬を表す Dog クラスや猫を表す Cat クラスはその振る舞いをわざわざ書き直さなくても、Animal を継承するだけで使えるようになります。コピペで増やしていたコードを 1 か所にまとめられるので、修正もテストも楽になります。
継承は「コードの再利用」のためだけにあるのではなく、「現実世界の is-a 関係をモデル化する」ためにあります。
Dog is an Animal(犬は動物の一種) のように、上位の概念と下位の概念が自然な親子関係になるときだけ使うのが鉄則です。
この考え方は Java だけのものではなく、Kotlin C# Swift など現代の言語に共通する基本です。とくに Android 開発で Activity を継承したり、ゲーム開発で GameObject を継承したりと、フレームワークの上で開発するときには継承の理解が必須になります。本レッスンでは Java の extends キーワードを軸に、継承の文法と落とし穴をしっかり押さえていきましょう。
上の図は Animal を親クラス、Dog と Cat を子クラスにした典型的な構図です。矢印は 継承 を表し、子から親に向かって伸びます。Dog も Cat も、自分で書いていない eat() や sleep() を呼び出せます。
extends の文法
Java で継承するときは、クラス名のうしろに extends キーワードを書いて親クラスを指定します。形は次のとおりです。
Java
class 親クラス名 {
// 共通の処理
}
class 子クラス名 extends 親クラス名 {
// 子だけの追加処理
}実際のコードで見てみましょう。
Java
class Animal {
public String eat() {
return "もぐもぐ";
}
}
class Dog extends Animal {
public String bark() {
return "Wan!";
}
}
public class Demo {
public static void main(String[] args) {
Dog d = new Dog();
System.out.println(d.eat()); // もぐもぐ (親から継承)
System.out.println(d.bark()); // Wan! (自分で定義)
}
}Dog には eat() を書いていないのに d.eat() が呼べる、というのが継承の効果です。extends Animal と書いた瞬間に、Animal の public および protected なメンバーがすべて Dog に流れ込んできます。private なメンバーだけは引き継がれず、親クラスの内部からしか触れません。
「
extendsは親の延長線」とイメージするのが分かりやすいです。DogはAnimalに追加機能を上乗せした「拡張版のAnimal」だと考えると、Dog d = new Dog();を作ったときにAnimalの機能もちゃんと含まれていることが腑に落ちます。
is-a 関係を満たさないものに extends を使わない
継承は強力ですが、使いどころを間違えるとあとから設計に苦しむ機能でもあります。判断基準は 子 is a 親 という文が日本語で自然に成り立つかどうか です。
Dog is an Animal→ 自然なのでDog extends Animalは OKCat is an Animal→ 自然なのでCat extends Animalは OKCar is an Engine→ 不自然 (車はエンジンを 持っている だけ) なのでextendsは NG。フィールドとしてEngineを持つコンポジションで設計するUser is a Database→ 不自然なので継承しない。Databaseをフィールドで受け取って使う方が素直
この判定をサボってフィールドを増やすつもりで継承してしまうと、子クラスに親クラスの無関係なメソッドが大量に生えてしまい、テストもリファクタリングも苦しくなります。
単一継承 — Java の親はいつもひとつだけ
Java の継承は 単一継承 と呼ばれ、ひとつのクラスが直接 extends できる親は 1 つだけ という決まりがあります。C++ のように複数の親を持つ 多重継承 は禁止されています。
Java
// NG: コンパイルエラー
class Hybrid extends Animal, Machine { }なぜこんな制限があるのかというと、もし Animal と Machine の両方に run() というメソッドがあったとき、Hybrid の run() がどちらを呼ぶのか曖昧になってしまうからです (これを ダイヤモンド問題 と呼びます)。Java はその曖昧さを最初から排除するために、親は 1 つしか持てないように設計されました。
ただし、implements を使った インタフェース の実装はいくつでも書けます。extends は 1 つだけ、implements は何個でも、と覚えておきましょう。
Java
class Robot extends Machine implements Walker, Talker {
// OK: 親は Machine だけ、振る舞いの契約は Walker と Talker から
}「コードを引き継ぐ」のが
extends、「契約だけ約束する」のがimplements、と棲み分けると混乱しません。本レッスンの課題はextendsを使うので、こちらに集中します。
継承関係を図で確認する
本レッスンの課題は Animal を Dog が継承するシンプルな構造です。クラス図にすると次のようになります。
Dog の側には bark() しか書いていなくても、Animal から eat() が降ってくる、というのがポイントです。Solution.dogBark() の中で new Dog() してから bark() を呼べば、"Wan!" という文字列が返ってきます。
よくある間違い
継承で初学者が踏みやすい地雷を 3 つ紹介します。事前に把握しておくと、エラーで詰まる時間がぐっと減ります。
extendsとimplementsの混同 — クラスを継承するときはextends、インタフェースを実装するときはimplementsを使います。class Dog implements Animalのように書いてしまうと、Animalがクラスの場合はコンパイルエラーになります。逆にinterface Walkerをextendsしようとしてもエラーです。「クラスから機能を引き継ぐならextends、契約だけ約束するならimplements」と覚えましょう- すべてのクラスが暗黙で
Objectを継承していることを忘れる — Java ではextendsを書かないクラスでも、自動的にjava.lang.Objectを継承しています。だからtoString()やequals()やhashCode()がどんなクラスでも呼べるわけです。Animal extends Objectと明示しても同じ意味ですが、わざわざ書かないのが普通です finalクラスを継承しようとする —StringIntegerLocalDateなどの標準クラスや、自分でfinal classと宣言したクラスは継承できません。class MyString extends Stringと書くとcannot inherit from final class Stringというコンパイルエラーになります。継承させたくないクラスにはあえてfinalを付ける、というのも設計上のテクニックです
もう一つ、コンストラクタ の挙動も最初は戸惑いやすいポイントです。子クラスのコンストラクタは、明示しなくても先頭で親クラスの引数なしコンストラクタ (super();) を呼びます。親に引数なしコンストラクタが無いと、子クラスで super(...) を明示しないとコンパイルエラーになる、というのが落とし穴です。本レッスンの課題ではどちらも引数なしのデフォルトコンストラクタで足りるので意識しなくても動きますが、頭の片隅に置いておきましょう。
エラーメッセージ
there is no default constructor available in Animalが出たら、親クラスに引数なしコンストラクタを足すか、子クラス側でsuper(...)を明示的に呼ぶ必要があります。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
- 業務エンティティの共通基底 ─
BaseEntityにidcreatedAtupdatedAtを集約し、UserOrderがextendsするパターン - フレームワークの拡張ポイント ─ Spring の
@ControllerAdviceや JUnit のTestCaseをextendsしてフックを差し込む - 例外クラスの階層 ─
MyAppExceptionを作り、NotFoundExceptionValidationExceptionがextendsする設計 - テンプレートメソッドパターン ─ 親クラスに大枠の処理を書き、可変部分だけを子で
overrideする設計
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
- 継承が深すぎる ─ 3 階層を超えると追跡が大変。委譲 (composition) で済むなら委譲を選ぶよう指摘される
extendsでis-aが成り立っていない ─Car extends Engineのように「持っている」を継承で表現していると指摘finalクラスをextendsしようとしている ─StringIntegerなどはfinal。コンパイルが通らない設計を出すと指摘- 子で
protectedをpublicに開きすぎ ─ アクセス修飾子を緩める変更は API 互換性の事故になりやすい
パフォーマンス考慮事項
- 仮想メソッド呼び出しのオーバーヘッド ─ 継承メソッドは vtable 経由になるが、JIT がインライン化するので実用上はほぼ無視できる
super()の暗黙呼び出し ─ 子のconstructorでsuper()を書かないと暗黙で呼ばれる。親の重い初期化を意識する- 深い継承は GC に不利 ─ 親クラスのフィールド全てがインスタンスに乗るため、メモリ常駐量が増える
instanceofチェーン ─ 多態をif (x instanceof A) ... else if (x instanceof B)で書くと遅い + 設計臭。多態 /sealedを使う
ここまでの要点
extends は単一継承で is-a 関係のとき。Object が暗黙の親、final は継承不可。深い階層より委譲を優先する。
やってみよう
それでは課題に挑戦してみましょう。今回は Solution.dogBark() メソッドを完成させます。やることは次の通りです。
Solutionクラスの中 (またはファイル内) に親クラスAnimalを作るAnimalをextendsしたDogクラスを作り、public String bark()を持たせるDog.bark()は"Wan!"を返すように実装するSolution.dogBark()の中でnew Dog()してからbark()を呼び、その結果をreturnする
テストは戻り値が "Wan!" (大文字 W + 小文字 an + 半角ビックリマーク) になっているかを比較します。"wan!" (小文字始まり) や "Wan" (末尾の ! 抜け) では fail するので注意してください。
ワンポイントとして、Dog クラス側に bark() を書く代わりに、Solution.dogBark() の中で直接 return "Wan!"; と返してもテストはたぶん通ってしまいます。ただしそれだと「継承の練習」になっていないので、必ず Dog extends Animal の構造を作り、new Dog().bark() を経由するようにしましょう。コードレビュー視点で見ても、extends を使っていないのに「継承の課題」と書かれているとモヤッとします。
慣れてきたら、Animal 側に eat() メソッドを足して Dog から呼んでみる、Cat extends Animal をもう 1 つ作ってみる、といった発展課題にも取り組んでみてください。次のレッスンでは、親と子で同じ名前のメソッドを定義する オーバーライド の使い方に踏み込んでいきます。
よくある質問
Q. 継承と委譲(composition)はどっちを使うべき?
A. 迷ったら委譲を選ぶのが現代の主流(Composition over Inheritance)です。継承は密結合になりやすく、親クラスの変更が子に波及します。委譲なら必要なメソッドだけ持つフィールドに任せるため、変更耐性と柔軟性が高くなります。
Q. オーバーライド時に super を呼ぶ必要はありますか?
A. 親の処理を維持したい場合は super.method() を最初に呼んでください。例えば onCreate(Bundle) のような Android のライフサイクルメソッドは super 呼び出しを忘れると例外になります。完全に置き換えたいなら super 呼び出しは不要です。
Q. 多重継承は Java でできますか?
A. クラスは 1 つしか継承できません(単一継承)。複数の振る舞いを持たせたいときは interface を複数 implements します。default メソッドで実装も持てるため、軽量な多重継承的な使い方が可能ですが、菱形継承の衝突は手動で解決する必要があります。
次のレッスン
次は super で親を呼ぶ で、super で親を呼ぶ を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- extends の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. extends とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- 親クラス
Animalと、それをextendsした子クラスDogの 2 つを定義すること Dogクラスにpublic String bark()メソッドを実装し、"Wan!"を返すことSolution.dogBark()の中でnew Dog()してからbark()を呼び、その結果をそのままreturnすること
入出力例
test-cases.txt
dogBark() → "Wan!"