7.継承
学習目標
- 共通する性質を親クラスにまとめ、子クラスで
extendsして引き継げる superで親のコンストラクターを呼び、メソッドをオーバーライドできる- 抽象クラスで、親のインスタンス化を禁じ、子に実装を強制できる
- 継承・インターフェイス・コンポジションを使い分けられる
1. クラスの親子関係
Section titled “1. クラスの親子関係”社員には技術職や管理職といった役職があります。役職によって持つ情報は違いますが、名前・基本給・自己紹介はどの社員にも共通します。技術職の Engineer はスキルを、管理職の Manager はチーム人数を、それぞれ社員共通の情報に加えて持ちます。
Engineer と Manager をそれぞれ単独のクラスとして定義すると、大部分のコードが共通します。
public class Engineer { private final String name; private final int baseSalary; private final String skill;
public Engineer(String name, int baseSalary, String skill) { this.name = name; this.baseSalary = baseSalary; this.skill = skill; }
public void introduce() { System.out.println(name + "(基本給: " + baseSalary + "円)"); }}public class Manager { private final String name; private final int baseSalary; private final int teamSize;
public Manager(String name, int baseSalary, int teamSize) { this.name = name; this.baseSalary = baseSalary; this.teamSize = teamSize; }
public void introduce() { System.out.println(name + "(基本給: " + baseSalary + "円)"); }}name、baseSalary フィールドと introduce メソッドが 2 つのクラスで同じなのは、Engineer も Manager も社員(Employee)の一種で、社員としての性質を共有するからです。共通する name、baseSalary、introduce を Employee という別のクラスに置き、Engineer と Manager がそれを引き継ぐ形に書き換えます。
2. extends とコンストラクター
Section titled “2. extends とコンストラクター”社員に共通する性質を持つクラス Employee を定義します。
public class Employee { protected final String name; protected final int baseSalary;
public Employee(String name, int baseSalary) { this.name = name; this.baseSalary = baseSalary; }
public void introduce() { System.out.println(name + "(基本給: " + baseSalary + "円)"); }}Engineer から Employee の性質を引き継ぐには、extends キーワードで Employee を指定します。
class クラス名 extends 引き継ぐクラス名 { // 独自の内容}public class Engineer extends Employee { private final String skill;
public Engineer(String name, int baseSalary, String skill) { super(name, baseSalary); this.skill = skill; }}Engineer には name、baseSalary フィールドや introduce メソッドを書いていませんが、Employee から引き継いでいるため使えます。Engineer が独自に持つのは skill だけです。
このように、あるクラスの性質を別のクラスが引き継ぐ仕組みを 継承 と呼びます。引き継がれる側の Employee を 親クラス(スーパークラス)、引き継ぐ側の Engineer を 子クラス(サブクラス)と呼びます。
Engineer engineer = new Engineer("田中", 300000, "Java");engineer.introduce();田中(基本給: 300000円)Manager も同じく Employee を継承し、固有の teamSize だけを追加します。
public class Manager extends Employee { private final int teamSize;
public Manager(String name, int baseSalary, int teamSize) { super(name, baseSalary); this.teamSize = teamSize; }}独立に書いていたときの name、baseSalary、introduce の重複が、Employee に集約されました。Engineer と Manager は、それぞれの固有のフィールドだけを書いています。
コンストラクターの 1 行目にある super(name, baseSalary) は、親クラスのコンストラクターを呼び出す 文です。Employee のコンストラクターに name と baseSalary を渡し、親クラス側でフィールドを初期化します。super(...) はコンストラクターの最初の行に書く必要があります。
3. メソッドのオーバーライド
Section titled “3. メソッドのオーバーライド”Employee から引き継いだ introduce は名前と基本給だけを表示します。Engineer の自己紹介にはスキルも含めます。
等価性 では Object のメソッドをオーバーライドしました。継承でも同じく、子クラスで親クラスと同じシグネチャ(名前・引数・戻り値の型)のメソッドを定義すると、親のメソッドが置き換わります。
public class Engineer extends Employee { private final String skill;
public Engineer(String name, int baseSalary, String skill) { super(name, baseSalary); this.skill = skill; }
@Override public void introduce() { System.out.println(name + "(基本給: " + baseSalary + "円, スキル: " + skill + ")"); }}Engineer engineer = new Engineer("田中", 300000, "Java");engineer.introduce();田中(基本給: 300000円, スキル: Java)Manager も同じく introduce をオーバーライドし、チーム人数を表示します。
@Overridepublic void introduce() { System.out.println(name + "(基本給: " + baseSalary + "円, チーム人数: " + teamSize + "人)");}Engineer と Manager は、同じ introduce というメソッドを、それぞれ別の内容でオーバーライドしています。
Employee の introduce ではなく、子クラスでオーバーライドした introduce が実行されます。introduce の中で name と baseSalary を参照できるのは、Employee でこれらを protected で宣言しているためです。
| 修飾子 | アクセスできる範囲 |
|---|---|
private | 同じクラスの中だけ |
protected | 同じクラスに加えて、子クラスからもアクセスできる |
public | どこからでも |
private のままだと子クラスから参照できないため、子クラスは getter を経由する必要があります。継承を前提とし、子クラスから参照させたいフィールドだけを protected にします。
3-1. 親のメソッドの呼び出し
Section titled “3-1. 親のメソッドの呼び出し”オーバーライドしたメソッドの中から、親クラスの同名メソッドを呼ぶには super.メソッド名() を使います。親の処理に追加して何かをしたい場合に使います。
@Overridepublic void introduce() { super.introduce(); // Employee の introduce を実行 System.out.println(" スキル: " + skill);}田中(基本給: 300000円) スキル: Javasuper.introduce() で親の introduce を実行し、そのあとにスキルを追加で出力しています。
4. 抽象クラス
Section titled “4. 抽象クラス”Employee は共通部分をまとめるための親クラスです。実際に存在するのは Engineer や Manager であり、Employee 自体のインスタンスを直接作る場面はありません。
このような「子クラスは存在するが、親クラス自体のインスタンスを作る意味がない」場合に、abstract を付けて 抽象クラス にします。
public abstract class Employee { protected final String name; protected final int baseSalary;
public Employee(String name, int baseSalary) { this.name = name; this.baseSalary = baseSalary; }
public void introduce() { System.out.println(name + "(基本給: " + baseSalary + "円)"); }}abstract を付けたクラスはインスタンスを作れません。
Employee employee = new Employee("佐藤", 400000); // コンパイルエラーEmployee を直接 new する方法がなくなり、Engineer や Manager を経由してのみ利用できます。継承のための親クラスであることが、コードの上で明確になります。
5. Object クラス
Section titled “5. Object クラス”Java では、すべてのクラスが暗黙のうちに Object クラス を継承しています。extends を書かないクラスも、内部的には extends Object と同じ扱いです。
等価性 で Person に equals、hashCode、toString をオーバーライドしました。当時は「Object というクラスをもとに作られている」とだけ説明しましたが、これは Person が Object を継承し、Object が持つこれらのメソッドをオーバーライドしていた、ということです。
equals、hashCode、toString は Object で定義されているため、どのクラスにも最初から備わっています。子クラスでオーバーライドしなければ、Object のデフォルトの実装(参照比較や、型名とハッシュコードを並べた文字列)が使われます。
6. 継承とインターフェイスの使い分け
Section titled “6. 継承とインターフェイスの使い分け”継承(extends)と、インターフェイス の実装(implements)は、どちらも「上位の型」を指定する仕組みですが、目的が違います。
| 項目 | extends(継承) | implements(インターフェイス実装) |
|---|---|---|
| 上位の型が持つもの | フィールドとメソッドの実装 | メソッドの宣言だけ |
| 子・実装側 | 親の実装をそのまま使える | 自分で実装を書く必要がある |
| 1 つのクラスから | 1 つの親クラスだけ継承できる | 複数のインターフェイスを実装できる |
継承は「親の実装を引き継いで拡張する」、インターフェイスの実装は「決められたメソッドを提供する」ことに重点があります。1 つのクラスは、extends で親を継承しつつ、implements で複数のインターフェイスを同時に実装できます。
継承を使うかどうかは、is-a 関係(〜は〜の一種)が成り立つか で判断します。
| 関係 | 適した手法 |
|---|---|
| 「Engineer は Employee の一種」(is-a 関係) | 継承(Engineer extends Employee) |
| 「GradeReportService は GradeJudge を使う」(has-a 関係) | コンポジション(フィールドとして持つ) |
「機能を借りたいだけ」の場合は、継承ではなく コンポジション(フィールドとして持って使う)を選びます。GradeReportService が GradeJudge を継承するのは不自然ですが、フィールドとして持つのは自然です。「A は B の一種」と読めるなら継承、「A は B を使う」と読めるならコンポジションです。