Skip to content
Playground

7.継承

学習目標

  • 共通する性質を親クラスにまとめ、子クラスで extends して引き継げる
  • super で親のコンストラクターを呼び、メソッドをオーバーライドできる
  • 抽象クラスで、親のインスタンス化を禁じ、子に実装を強制できる
  • 継承・インターフェイス・コンポジションを使い分けられる

社員には技術職や管理職といった役職があります。役職によって持つ情報は違いますが、名前・基本給・自己紹介はどの社員にも共通します。技術職の Engineer はスキルを、管理職の Manager はチーム人数を、それぞれ社員共通の情報に加えて持ちます。

EngineerManager をそれぞれ単独のクラスとして定義すると、大部分のコードが共通します。

Engineer.java
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 + "円)");
}
}

namebaseSalary フィールドと introduce メソッドが 2 つのクラスで同じなのは、EngineerManager も社員(Employee)の一種で、社員としての性質を共有するからです。共通する namebaseSalaryintroduceEmployee という別のクラスに置き、EngineerManager がそれを引き継ぐ形に書き換えます。

社員に共通する性質を持つクラス Employee を定義します。

Employee.java
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 引き継ぐクラス名 {
// 独自の内容
}
Engineer.java
public class Engineer extends Employee {
private final String skill;
public Engineer(String name, int baseSalary, String skill) {
super(name, baseSalary);
this.skill = skill;
}
}

Engineer には namebaseSalary フィールドや introduce メソッドを書いていませんが、Employee から引き継いでいるため使えます。Engineer が独自に持つのは skill だけです。

このように、あるクラスの性質を別のクラスが引き継ぐ仕組みを 継承 と呼びます。引き継がれる側の Employee親クラス(スーパークラス)、引き継ぐ側の Engineer子クラス(サブクラス)と呼びます。

Employee と Engineer の継承関係

Main.java
Engineer engineer = new Engineer("田中", 300000, "Java");
engineer.introduce();
田中(基本給: 300000円)

Manager も同じく Employee を継承し、固有の teamSize だけを追加します。

Manager.java
public class Manager extends Employee {
private final int teamSize;
public Manager(String name, int baseSalary, int teamSize) {
super(name, baseSalary);
this.teamSize = teamSize;
}
}

独立に書いていたときの namebaseSalaryintroduce の重複が、Employee に集約されました。EngineerManager は、それぞれの固有のフィールドだけを書いています。

コンストラクターの 1 行目にある super(name, baseSalary) は、親クラスのコンストラクターを呼び出す 文です。Employee のコンストラクターに namebaseSalary を渡し、親クラス側でフィールドを初期化します。super(...) はコンストラクターの最初の行に書く必要があります。

Employee から引き継いだ introduce は名前と基本給だけを表示します。Engineer の自己紹介にはスキルも含めます。

等価性 では Object のメソッドをオーバーライドしました。継承でも同じく、子クラスで親クラスと同じシグネチャ(名前・引数・戻り値の型)のメソッドを定義すると、親のメソッドが置き換わります。

Engineer.java
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 + "");
}
}
Main.java
Engineer engineer = new Engineer("田中", 300000, "Java");
engineer.introduce();
田中(基本給: 300000円, スキル: Java)

Manager も同じく introduce をオーバーライドし、チーム人数を表示します。

Manager.java
@Override
public void introduce() {
System.out.println(name + "(基本給: " + baseSalary + "円, チーム人数: " + teamSize + "人)");
}

EngineerManager は、同じ introduce というメソッドを、それぞれ別の内容でオーバーライドしています。

Employeeintroduce ではなく、子クラスでオーバーライドした introduce が実行されます。introduce の中で namebaseSalary を参照できるのは、Employee でこれらを protected で宣言しているためです。

修飾子アクセスできる範囲
private同じクラスの中だけ
protected同じクラスに加えて、子クラスからもアクセスできる
publicどこからでも

private のままだと子クラスから参照できないため、子クラスは getter を経由する必要があります。継承を前提とし、子クラスから参照させたいフィールドだけを protected にします。

オーバーライドしたメソッドの中から、親クラスの同名メソッドを呼ぶには super.メソッド名() を使います。親の処理に追加して何かをしたい場合に使います。

Engineer.java
@Override
public void introduce() {
super.introduce(); // Employee の introduce を実行
System.out.println(" スキル: " + skill);
}
田中(基本給: 300000円)
スキル: Java

super.introduce() で親の introduce を実行し、そのあとにスキルを追加で出力しています。

Employee は共通部分をまとめるための親クラスです。実際に存在するのは EngineerManager であり、Employee 自体のインスタンスを直接作る場面はありません。

このような「子クラスは存在するが、親クラス自体のインスタンスを作る意味がない」場合に、abstract を付けて 抽象クラス にします。

Employee.java
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 を付けたクラスはインスタンスを作れません。

Main.java
Employee employee = new Employee("佐藤", 400000); // コンパイルエラー

Employee を直接 new する方法がなくなり、EngineerManager を経由してのみ利用できます。継承のための親クラスであることが、コードの上で明確になります。

Java では、すべてのクラスが暗黙のうちに Object クラス を継承しています。extends を書かないクラスも、内部的には extends Object と同じ扱いです。

等価性PersonequalshashCodetoString をオーバーライドしました。当時は「Object というクラスをもとに作られている」とだけ説明しましたが、これは PersonObject を継承し、Object が持つこれらのメソッドをオーバーライドしていた、ということです。

equalshashCodetoStringObject で定義されているため、どのクラスにも最初から備わっています。子クラスでオーバーライドしなければ、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 関係)コンポジション(フィールドとして持つ)

「機能を借りたいだけ」の場合は、継承ではなく コンポジション(フィールドとして持って使う)を選びます。GradeReportServiceGradeJudge を継承するのは不自然ですが、フィールドとして持つのは自然です。「A は B の一種」と読めるなら継承、「A は B を使う」と読めるならコンポジションです。