extends で継承する

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • class Dog extends Animal で既存クラスの機能を引き継ぐ
  • extends できるのは 1 つの親クラスだけ (多重継承は不可)。複数の機能は interface で取り込む
  • private フィールドは継承しても子から直接触れない。protected か getter 経由でアクセス
  • 全クラスは暗黙に Object を継承していて、toString equals hashCode を持つ

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 キーワードを軸に、継承の文法と落とし穴をしっかり押さえていきましょう。

diagram (will load when visible)

上の図は Animal を親クラス、DogCat を子クラスにした典型的な構図です。矢印は 継承 を表し、子から親に向かって伸びます。DogCat も、自分で書いていない 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 と書いた瞬間に、Animalpublic および protected なメンバーがすべて Dog に流れ込んできます。private なメンバーだけは引き継がれず、親クラスの内部からしか触れません。

extends は親の延長線」とイメージするのが分かりやすいです。DogAnimal に追加機能を上乗せした「拡張版の Animal」だと考えると、Dog d = new Dog(); を作ったときに Animal の機能もちゃんと含まれていることが腑に落ちます。

is-a 関係を満たさないものに extends を使わない

継承は強力ですが、使いどころを間違えるとあとから設計に苦しむ機能でもあります。判断基準は 子 is a 親 という文が日本語で自然に成り立つかどうか です。

  • Dog is an Animal → 自然なので Dog extends Animal は OK
  • Cat is an Animal → 自然なので Cat extends Animal は OK
  • Car is an Engine → 不自然 (車はエンジンを 持っている だけ) なので extends は NG。フィールドとして Engine を持つ コンポジション で設計する
  • User is a Database → 不自然なので継承しない。Database をフィールドで受け取って使う方が素直

この判定をサボってフィールドを増やすつもりで継承してしまうと、子クラスに親クラスの無関係なメソッドが大量に生えてしまい、テストもリファクタリングも苦しくなります。

単一継承 — Java の親はいつもひとつだけ

Java の継承は 単一継承 と呼ばれ、ひとつのクラスが直接 extends できる親は 1 つだけ という決まりがあります。C++ のように複数の親を持つ 多重継承 は禁止されています。

Java

// NG: コンパイルエラー class Hybrid extends Animal, Machine { }

なぜこんな制限があるのかというと、もし AnimalMachine の両方に run() というメソッドがあったとき、Hybridrun() がどちらを呼ぶのか曖昧になってしまうからです (これを ダイヤモンド問題 と呼びます)。Java はその曖昧さを最初から排除するために、親は 1 つしか持てないように設計されました。

ただし、implements を使った インタフェース の実装はいくつでも書けます。extends は 1 つだけ、implements は何個でも、と覚えておきましょう。

Java

class Robot extends Machine implements Walker, Talker { // OK: 親は Machine だけ、振る舞いの契約は Walker と Talker から }

「コードを引き継ぐ」のが extends、「契約だけ約束する」のが implements、と棲み分けると混乱しません。本レッスンの課題は extends を使うので、こちらに集中します。

継承関係を図で確認する

本レッスンの課題は AnimalDog が継承するシンプルな構造です。クラス図にすると次のようになります。

diagram (will load when visible)

Dog の側には bark() しか書いていなくても、Animal から eat() が降ってくる、というのがポイントです。Solution.dogBark() の中で new Dog() してから bark() を呼べば、"Wan!" という文字列が返ってきます。

よくある間違い

継承で初学者が踏みやすい地雷を 3 つ紹介します。事前に把握しておくと、エラーで詰まる時間がぐっと減ります。

  • extendsimplements の混同 — クラスを継承するときは extendsインタフェース を実装するときは implements を使います。class Dog implements Animal のように書いてしまうと、Animal がクラスの場合はコンパイルエラーになります。逆に interface Walkerextends しようとしてもエラーです。「クラスから機能を引き継ぐなら extends、契約だけ約束するなら implements」と覚えましょう
  • すべてのクラスが暗黙で Object を継承していることを忘れる — Java では extends を書かないクラスでも、自動的に java.lang.Object を継承しています。だから toString()equals()hashCode() がどんなクラスでも呼べるわけです。Animal extends Object と明示しても同じ意味ですが、わざわざ書かないのが普通です
  • final クラスを継承しようとするString Integer LocalDate などの標準クラスや、自分で final class と宣言したクラスは継承できません。class MyString extends String と書くと cannot inherit from final class String というコンパイルエラーになります。継承させたくないクラスにはあえて final を付ける、というのも設計上のテクニックです

もう一つ、コンストラクタ の挙動も最初は戸惑いやすいポイントです。子クラスのコンストラクタは、明示しなくても先頭で親クラスの引数なしコンストラクタ (super();) を呼びます。親に引数なしコンストラクタが無いと、子クラスで super(...) を明示しないとコンパイルエラーになる、というのが落とし穴です。本レッスンの課題ではどちらも引数なしのデフォルトコンストラクタで足りるので意識しなくても動きますが、頭の片隅に置いておきましょう。

エラーメッセージ there is no default constructor available in Animal が出たら、親クラスに引数なしコンストラクタを足すか、子クラス側で super(...) を明示的に呼ぶ必要があります。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • 業務エンティティの共通基底BaseEntityid createdAt updatedAt を集約し、User Orderextends するパターン
  • フレームワークの拡張ポイント ─ Spring の @ControllerAdvice や JUnit の TestCaseextends してフックを差し込む
  • 例外クラスの階層MyAppException を作り、NotFoundException ValidationExceptionextends する設計
  • テンプレートメソッドパターン ─ 親クラスに大枠の処理を書き、可変部分だけを子で override する設計

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • 継承が深すぎる ─ 3 階層を超えると追跡が大変。委譲 (composition) で済むなら委譲を選ぶよう指摘される
  • extendsis-a が成り立っていないCar extends Engine のように「持っている」を継承で表現していると指摘
  • final クラスを extends しようとしているString Integer などは final。コンパイルが通らない設計を出すと指摘
  • 子で protectedpublic に開きすぎ ─ アクセス修飾子を緩める変更は API 互換性の事故になりやすい

パフォーマンス考慮事項

  • 仮想メソッド呼び出しのオーバーヘッド ─ 継承メソッドは vtable 経由になるが、JIT がインライン化するので実用上はほぼ無視できる
  • super() の暗黙呼び出し ─ 子の constructorsuper() を書かないと暗黙で呼ばれる。親の重い初期化を意識する
  • 深い継承は GC に不利 ─ 親クラスのフィールド全てがインスタンスに乗るため、メモリ常駐量が増える
  • instanceof チェーン ─ 多態を if (x instanceof A) ... else if (x instanceof B) で書くと遅い + 設計臭。多態 / sealed を使う
この章のポイント

ここまでの要点 extends は単一継承で is-a 関係のとき。Object が暗黙の親、final は継承不可。深い階層より委譲を優先する。

やってみよう

それでは課題に挑戦してみましょう。今回は Solution.dogBark() メソッドを完成させます。やることは次の通りです。

  1. Solution クラスの中 (またはファイル内) に親クラス Animal を作る
  2. Animalextends した Dog クラスを作り、public String bark() を持たせる
  3. Dog.bark()"Wan!" を返すように実装する
  4. 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 つができることを確認しましょう。

  1. extends の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. extends とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. 親クラス Animal と、それを extends した子クラス Dog の 2 つを定義すること
  2. Dog クラスに public String bark() メソッドを実装し、"Wan!" を返すこと
  3. Solution.dogBark() の中で new Dog() してから bark() を呼び、その結果をそのまま return すること

入出力例

test-cases.txt

dogBark()"Wan!"

ヒント

main.java
main.java
学習モード

メモ

extends で継承する

⌘S で保存