Chapter 21

継承・ポリモーフィズム

KeitoYuasa
KeitoYuasa
2023.02.25に更新

継承

No. 要点
1 サブクラスをインスタンス化すると、スーパークラスで定義したメンバを引き継ぐ(親の実体が生成される)。
2 スーパークラスでprivate指定されたメンバは、サブクラスに引き継がれない。
3 1クラス1スーパークラスのみ指定可。(extendsの後ろには1クラスしか指定できない)
4 extendsを指定せずにクラス定義した場合、暗黙でjava.lang.Objectクラスをスーパークラスに持つ。

java.lang.Objectクラス:スレッドの制御やオブジェクトのコピーなど、全てのオブジェクトに共通で提供すべきメソッドが定義されている。

継承の定義

  • 定義:[修飾子] class サブクラス名 extends スーパークラス名{}
  • 1クラス1スーパークラスのみ指定可。(extendsの後ろには1クラスしか指定できない)
  • extendsを指定せずにクラス定義した場合、暗黙でjava.lang.Objectクラスをスーパークラスに持つ(つまり、シンプルに定義したクラスでもjava.lang.Objectクラスを親に持つ)。
//スーパークラス
class SuperA {}
class SuperB {}

//サブクラス
class SubA extends SuperA {}
class SubB extends SuperA {}
class SubC extends SubA {}
//class SubD extends SuperA, SuperB {} コンパイルエラー

※SuperA, SuperBクラスは、extends指定していないが、java.lang.Objectクラスを継承している。また、上記では、subA, subB, SubCなどのサブクラスもjava.lang.Objectクラスのメンバを引き継ぐ。

Training.java
//スーパークラス
class Employee {
    private String id = "100";
    public String getId() {
        return id;
    }
}

//サブクラス
class Sales extends Employee {
    private String clientName = "SE";
    public String getClientName(){
        return clientName;
    }
}

//実行クラス
class Training {
    public static void main(String[] args){
        Sales s = new Sales(); //サブクラスをインスタンス化

        //サブクラスのメソッドをコール
        System.out.println(s.getClientName());
        //System.out.println(s.clientName); コンパイルエラー(privateメンバにはアクセス不可)
        
        //スーパークラスのメソッドをコール
        System.out.println(s.getId());
        //System.out.println(s.id); コンパイルエラー(privateメンバにはアクセス不可)
    }
}

//出力結果:
//SE
//100

※privateメンバは、同クラス内でしかアクセスすることができない。外部クラスからアクセスはできない。

オーバーライド

  • オーバーライド:サブクラス内で、スーパークラスで定義しているメソッドと同名でメソッドを再定義すること。(スーパークラスのメソッドの機能を拡張、修正する際に用いる)
  • サブクラスをインスタンス化し、オーバーライドしたメソッドをコールする際は、サブクラスのものが優先される。
No. 要点
1 サブクラスをインスタンス化し、オーバーライドしたメソッドをコールする際は、サブクラスのものが優先される。
2 アクセス修飾子:スーパークラスのメソッドと同じか、それより公開範囲が広いものを指定する必要がある。
3 戻り値:スーパークラスのメソッドと同じ型か、その型のサブクラス型でなくてはいけない。
4 メソッド名、引数:スーパークラスと同じ名前と引数にする必要がある。
5 インスタンスメソッドをstaticメソッドでオーバーライドすることはできない。その逆もできない。あくまでもスーパークラスのメソッドと同じ物のみ可。

以下では、オーバーライドの例を示す。
アクセス修飾子がスーパークラスのものより公開範囲が狭いものはコンパイルエラーになっている。
なお、同一クラスのメソッドを複製するのではなく、スーパークラスのメソッドを上書きしているためSubA.method()はオーバーライドとして扱われる。

Training.java
//スーパークラス
class SuperA {
    public void print(String s){
        System.out.println("スーパークラスのメソッド:" + s);
    }

    public void method(){}
}

//サブクラス
class SubA extends SuperA {
    //オーバーライド
    public void print(String s){
        System.out.println("サブクラスのメソッド:" + s);
    }

    //void method(){} コンパイルエラー(アクセス修飾子がデフォルトなので)
}

//実行クラス
class Training {
    public static void main(String[] args){
        SuperA superA = new SuperA();
        superA.print("テスト"); //自分のメソッドを実行

        SubA subA = new SubA();
        subA.print("テスト"); //自分のメソッドを実行
    }
}

//出力結果:
//スーパークラスのメソッド:テスト
//サブクラスのメソッド:テスト

オーバーロード・オーバーライドの違い

違い 説明
オーバーロード 引数の数、型、順番を変えることで同一クラスに同名メソッドを複数定義する機能。そのため、同一クラス内のメソッドを複数定義できる。
オーバーライド アクセス修飾子、戻り値に制約があり、かつメソッド名、引数が全く同じまま、スーパークラスから継承したメソッドをサブクラスで再定義する機能。そのため、同一クラスに複数定義はできない。

オーバーライドアノテーション

  • オーバーライドアノテーション:@Overrideをメソッドに付与することで、オーバーライドしていることをコンパイラに知らせるもの。
  • オーバーライドを行う際は、オーバーライドアノテーションをつけることを強く推奨
  • オーバーライドしていたつもりという、うっかりミスを事前に防ぐことができる。

以下のスーパークラスがあるとする。

Training.java
//スーパークラス
class SuperA {
    public void print(String s){
        System.out.println("スーパークラスのメソッド:" + s);
    }
}

上記を継承したサブクラスを定義する。
以下では、スーパークラスのメソッドと引数の型が異なるので、オーバーライドとみなされない。
つまり、コンパイラは新規メソッドとして認識する。そのため、エラーも表示されない。

Training.java
//サブクラス
class SubA extends SuperA {
    public void print(int s){
        System.out.println("サブクラスのメソッド:" + s);
    }
}

また、以下の場合もメソッド名のスペルが異なるため、オーバーライドとみなされない。
これらのような、オーバーライドしたつもりができていないといううっかりミスが起きてしまう。

Training.java
//サブクラス
class SubA extends SuperA {
    public void printt(String s){
        System.out.println("サブクラスのメソッド:" + s);
    }
}

@Overrideアノテーションの使用方法

上記の例をもとにアノテーションを使う。
以下のコードは、上記と同様にぱっと見オーバーライドできているかのように思える。
そこで、うっかりミスをなくすために、オーバーライドしたメソッドの上に@Overrideを付与した。
そのため、コンパイラはオーバーライドしているメソッドと認識するため、メソッド名が異なることからコンパイルエラーを吐いてくれる。
これで、うっかりミスを防ぐことができる。
これらの理由から、オーバーライドを行う際は、アノテーションを用いることを強く推奨する。

Training.java
//サブクラス
class SubA extends SuperA {
        @Override
    public void printt(String s){
        System.out.println("サブクラスのメソッド:" + s);
    }
}

final修飾子 (その他修飾子)

finalは、クラス、メンバ変数、メソッド、ローカル変数などに指定することができる。
※必ずアクセス修飾子を先頭に指定すること。その他修飾子はその次。
※staticは、クラス、メンバ変数、メソッドなどに指定できるがローカル変数には指定できない(staticはクラス変数なので)。

No. 要点
1 クラスに指定した場合、そのクラスをもとにサブクラスを生成できない。つまり、finalクラスの継承は不可。
2 メソッドに指定した場合、サブクラス側でそのメソッドをオーバーライドできなくなる。
class SuperA{}
final class SuperB{}
class SuperC{void print(){}}
class SuperD{final void print(){}}

class SubA extends SuperA{}
//class SubB extends SuperB{} コンパイルエラー
class SubC extends SuperC{void print(){}}
//class SubD extends SuperD{coid print(){}} コンパイルエラー

thisとsuper

this

  • this:自分自身(自分オブジェクト)を表す。つまり、インスタンスメンバを指す。
    • this.変数名:インスタンス変数を指す。
    • this.メソッド名:インスタンスメソッドを指す。
    • this():コンストラクタを指す。

メソッドの引数で宣言したローカル変数とインスタンス変数の名前が同じ場合

メソッドの引数で宣言したローカル変数とインスタンス変数の名前が同じ場合、thisを用いないと以下の問題が起こる。

  • 以下では、メンバ変数とメソッドでそれぞれ別のスコープであることを理解しておく必要がある。
  • 次に、メソッド内のid変数(ローカル変数)には、メソッドの引数の値を代入している。
  • つまり、メソッドより外のスコープにあるメンバ変数idには代入されない。
  • なぜなら、同じスコープの変数(引数として宣言している変数)が優先されるためだ。
  • 結果、メソッドに渡す値を引数に代入しているという意味のないことを行なっている。
thisを用いない場合
int id;
void setId (int id){
  id = id;
}

上記の問題を解決するために、thisを使う。
thisは、自分自身を表す。つまり、this.変数名の場合はインスタンス変数を指す。
そのため、以下では、引数で受け取った値をインスタンス変数に代入していることになる。

thisを使用
int id;
void setId (int id){
  this.id = id;
}

this()

  • this():コンストラクタを表す。コンストラクタ内で、かつその先頭に定義する必要がある。コンストラクタからゾクラス内で定義した別のコンストラクタをコールする際に用いる。

以下では、this()を用いた例を示す。this()を使って、時クラス内の他のコンストラクタをコールしていることがわかる。
自分自身コンストラクタのコールは、コンパイルエラー(2回初期化を行うことになるので)。

Training.java
//設計図クラス
class Foo {
    String str; int num;

    public Foo() {
        //this(); コンパイルエラー
        this("データなし");
    }
    public Foo(String s) {
        this(s, 1);
    }
    public Foo(String s, int i){
        this.str = s; this.num = i;
        System.out.println(this.str);
        System.out.println(this.num);
    }
}

//実行クラス
class Training {
    public static void main(String[] args){
        Foo f1 = new Foo();
        Foo f2 = new Foo("データ2");
        Foo f3 = new Foo("データ3", 500);

    }
}

super

  • super:自分自身(自分オブジェクト)のスーパークラスのオブジェクトを表す。つまり、親のメンバを指す。
    • super.変数名:親のメンバ変数を指す。
    • super.メソッド名:親のメソッドを指す。
    • super():親のコンストラクタを指す。

superの使用

superは、サブクラス側でメソッドをオーバーライドしていて、かつスーパークラスで定義していた機能を使いたいときに使用する。

以下では、superの使用例を示す。
そもそも、スーパークラスのものを全て継承するので、superが不要かと思う。
しかし、以下のように、オーバーライドしていているが親の機能を用いたい場合にsuperを用いる。

Training.java
//設計図クラス
//スーパークラス
class SuperA {
    int num;

    public void methodA(){ num += 100; }
    public void print() { System.out.println("数値:" + num); }
}
//サブクラス
class SubA extends SuperA {
    public void methodA() { num += 500;}
    public void methodB() {
        methodA();
        print(); //親のメソッドを実行
        super.methodA(); //親のメソッドを実行
        print(); //親のメソッドを実行
    }
}

//実行クラス
class Training {
    public static void main(String[] args){
        SubA s = new SubA();
        s.methodB();

    }
}
//出力結果:
//数値:500
//数値:600

※メンバ変数の場合、データ型に応じて変数に初期値が代入されることを忘れずに

①コンストラクタの呼び出し(superの指定なし)

サブクラスをインスタンス化すると、まずスーパークラスのことンストラクタ(super())が実行されてからサブクラスのコンストラクタが実行される。

No. 要点
1 指定がなくても、super()が実行されてから、サブクラスのコンストラクタが実行される。
Training.java
//スーパークラス
class SuperA {
    public SuperA() { System.out.println("SuperA"); }
    public SuperA(int i) { System.out.println("SuperA int:" + i); }
}
//サブクラス
class SubA extends SuperA {
    //親のコンストラクタを実行(super())
    public SubA(){ System.out.println("SubA"); }
    public SubA(int i){ System.out.println("SubA int:" + i); }
}

//実行クラス
class Training {
    public static void main(String[] args){
        SubA s1 = new SubA();
        SubA s2 = new SubA(500);

    }
}
//出力結果:
//SuperA
//SubA
//SuperA
//SubA int:500

②コンストラクタの呼び出し(superの指定)

No. 要点
1 super(引数)を指定することで、実行するスーパークラスのコンストラクタを選ぶことができる。
this()と同様にコンストラクタの先頭に記述する必要がある。
Training.java
//スーパークラス
class SuperA {
    public SuperA() { System.out.println("SuperA"); }
    public SuperA(int i) { System.out.println("SuperA int:" + i); }
}
//サブクラス
class SubA extends SuperA {
    public SubA(){ System.out.println("SubA"); }
    public SubA(int i){
        super(i); //親のint型を受け取るコンストラクタを実行
        System.out.println("SubA int:" + i);
    }
}

//実行クラス
class Training {
    public static void main(String[] args){
        SubA s1 = new SubA();
        SubA s2 = new SubA(500);

    }
}
//出力結果:
//SuperA
//SubA
//SuperA int:500
//SubA int:500

thisとsuperの順序

thisとsuperは、どちらともコンストラクタの先頭に定義する必要がある。
そのため、以下の場合コンパイルエラー。また、その逆もコンパイルエラー。

コンパイルエラーの例
public Sub(int i){
  super(i);
  this();
}
コンパイルエラーの例
public Sub(int i){
  this();
  super(i);
}

抽象クラス

  • 抽象クラス(abstractクラス):インスタンス化できないクラス。また、抽象メソッドを定義しているクラス。
  • 定義:[アクセス修飾子] abstract class クラス名{}
クラス 違い
具象クラス これまで扱ってきたクラス。インスタンス化できる。処理内容を記述したメソッドを定義できる。
抽象クラス インスタンス化できない。処理内容が記述していない抽象メソッド(abstractメソッド)、または処理内容を記述したメソッドの両方を混在して定義できる。
No. 要点
1 インスタンス化できない。つまり、サブクラスを生成し、実装することとを前提に定義する。
2 抽象メソッドを定義する場合、抽象クラス内でしか定義できない。
3 抽象クラス内では、具象メソッド、抽象メソッドを混在して定義することができる。
4 抽象クラスを利用する際は、そのサブクラスを生成し、インスタンス化する。
5 抽象クラスを継承したサブクラスが具象クラスの場合、親の抽象メソッドを全てオーバーライドしなければいけない。
6 抽象クラスを継承したサブクラスが抽象クラスの場合、親の抽象メソッドのオーバーライドは任意。

抽象メソッド

  • 抽象メソッド(abstractメソッド):中身を定義しない、からのメソッド。そのため、ブロックは書かない。
  • 定義:[アクセス修飾子] abstract 戻り値 メソッド名(引数);

抽象クラスを継承したクラス

抽象クラスを親として、具象クラスに継承した場合、親が持つ抽象メソッドを全てオーバーライドする必要がある。(なぜなら、抽象クラスでは、継承する各サブクラスで共通する機能(メソッド)を空の状態で定義しておくため。つまり、サブクラス側で親の抽象メソッドを実装していくことを前提に抽象クラスを定義する。)

  • 実装:抽象メソッドをオーバーライドして処理内容を記述、拡張することを実装と呼ぶ。

①抽象クラス→具象クラスの継承

  • 抽象クラスはインスタンス化できない。
  • 抽象クラスを継承した具象クラスでは、親のabstractメソッドを全て実装する必要がある。(しないとコンパイルエラー)
  • また、抽象クラスの具象メソッドは、これまでと同様に実装ではなくオーバーライド。つまり、オーバーライドは任意で親のものを受け継ぐ。
  • 実装は、抽象メソッドをオーバーライドすることなので、オーバーライドのルールが適用される。(アクセス修飾子は親と同じか、それ以上の公開範囲のものならオーバーライドとみなされる。)
Training.java
//抽象クラス
abstract class SuperA {
    abstract void methodA(); //abstractメソッド
    public void methodB(){ System.out.println("抽象クラスの具象メソッド"); } //具象メソッド
}
//サブクラス(具象クラス)
class SubA extends SuperA {
    @Override
    void methodA(){ System.out.println("実装"); } //実装(抽象メソッドのオーバーライド)
    //public void methodA(){ System.out.println("実装"); } アクセス修飾子が同じ、公開範囲が広いものはok

    //public void methodB(){ System.out.println("ただのオーバーライド。"); } //任意
}

//実行クラス
class Training {
    public static void main(String[] args){
        SubA s = new SubA();
        s.methodA();
        s.methodB();

        //SuperA a = new SuperA(); コンパイルエラー(抽象クラスはインスタンス化できない)
    }
}
//出力結果:
//実装
//抽象クラスの具象メソッド

②抽象クラス→抽象クラスの継承

  • 抽象クラスを継承した抽象クラスでは、abctractメソッドの実装は、任意。
  • 抽象クラスを継承した具象クラスでは、abstractメソッドの実装は、必須。
Training.java
//抽象クラス
abstract class SuperA {
    abstract void methodA(); //abstractメソッド
    public void methodB(){ System.out.println("抽象クラスの具象メソッド"); } //具象メソッド
}

//サブクラス(抽象クラス)
abstract class SubA extends SuperA{}

//サブクラス(具象クラス)
class SubB extends SubA {
    @Override
    void methodA(){ System.out.println("実装"); } //実装(抽象メソッドのオーバーライド)
    //public void methodA(){ System.out.println("実装"); } アクセス修飾子が同じ、公開範囲が広いものはok

    //public void methodB(){ System.out.println("ただのオーバーライド。"); } //任意
}

//実行クラス
class Training {
    public static void main(String[] args){
        SubB s = new SubB();
        s.methodA();
        s.methodB();
    }
}
//出力結果:
//実装
//抽象クラスの具象メソッド

インターフェース

  • インターフェース:様々な便利な機能をまとめたクラス
  • 定義:[アクセス修飾子] interface インターフェース名 {}
No. 要点
1 インスタンス化できない。つまり、サブクラスを生成し、実装することを前提に定義する。
2 インターフェースをもとに実装クラスを定義する際は、implementsキーワードを使用する。
3 インターフェースをもとにサブインターフェースを生成する際は、extendsキーワードを使用する。
4 インターフェース内では、抽象メソッド、具象メソッド、staticメソッドを混在して定義することができる。
5 インターフェース内で宣言できる変数はstaticな定数のみ。(インスタンス変数は宣言できない)
6 インスタンス変数を含め全ての変数は、強制的にpublic static final修飾子が付与される。
7 変数を宣言時に初期化しないといけない。(しない場合はコンパイルエラー)

インターフェース内の変数

  • staticな定数(final)しか宣言できない。
  • インスタンス変数を含め全ての変数は、強制的にpublic static final修飾子が付与される。
  • また、定数しか定義できないので、定数の宣言時に初期化しないとコンパイルエラーになる。
コンパイル前
interface Test{
    //int a; コンパイルエラー
    int b = 10;
    static int c = 20;
    public final int d = 30;
}
コンパイル後
interface Test{
    public static final int b = 10;
    public static final int c = 20;
    public static final int d = 30;
}

インターフェース内のメソッド

  • abstractメソッド、具象メソッド、staticメソッドを定義できる。

抽象メソッド(abstractメソッド)

  • 全てのメソッドは、強制的にpublic abstract修飾子が付与される。
  • また、public以下のアクセス修飾子を指定した場合、コンパイルエラーになる。インターフェースは、どこからのクラスでも用いることを前提に定義するため。
コンパイル前
interface Test{
    public abstract void methodA();
    abstract void methodB();
    void methodC();
    //protected abstract void methodD(); コンパイルエラー
}
コンパイル後
interface Test{
    public abstract void methodA();
    public abstract void methodB();
    public abstract void methodC();
}

具象メソッド(デフォルトメソッド)

  • SE8からインターフェース内で具象メソッドが定義できるようになった。
  • インターフェース内では、具象メソッドをデフォルトメソッドと呼ぶ。
  • 定義:[アクセス修飾子] default 戻り値 メソッド名(引数){処理;}
  • 指定できるアクセス修飾子はpublicのみ。
  • アクセス修飾子を指定していない場合は、強制的にpublic修飾子が付与される。

staticメソッド

  • SE8からインターフェース内でstaticメソッド(staticな具象メソッド)が定義できるようになった。
  • アクセス修飾子を指定していない場合は、強制的にpublic修飾子が付与される。
  • private修飾子は指定可能。
  • protectedは指定できない。コンパイルエラー。

インターフェースの実装クラス

  • インターフェースは、インスタンス化できないため、定義したものを使用する場合、クラスから読み込んで抽象メソッドをオーバーライドする必要がある。
  • このインターフェースを読み込むクラスを実装クラスと呼ぶ。(実装する=implements)
  • 実装クラスの定義:[修飾子] class クラス名 implements インターフェース名{}
  • 継承とは違って、implementsの後に複数のインターフェースを指定することができる。
  • 実装クラスを作る際の注意点:
    • implementsキーワードで指定した全てのインターフェースの抽象メソッドをオーバーライドする必要がある。
    • オーバーライドする際は、public修飾子をつける必要がある。(付けない場合は、デフォルトの公開範囲になるため)
Training.java
//インターフェース1
interface Inter1 {
    double methodA(int i);
    default void methodB(){ System.out.println("methodB"); }
}

//インターフェース2
interface Inter2 {
    int methodC(int i1, int i2);
    static void methodD() { System.out.println("methodD"); }
}

//実装クラス
class Test implements Inter1, Inter2 {
    public double methodA(int i){ return i + 10; }
    public int methodC(int i1, int i2){ return i1 + i2; }
}

//実行クラス
class Training {
    public static void main(String[] args){
        Test t = new Test();
        
        System.out.println("methodA:" + t.methodA(1));
        System.out.println("methodC:" + t.methodC(1, 2));

        t.methodB();
        Inter2.methodD();

        //t.methodD(); コンパイルエラー(拡張したものではないので、実装クラスにはない。)
    }
}
//出力結果:
//methodA:11.0
//methodC:3
//methodB
//methodD

インターフェースは、クラス。機能の提供、その機能を拡張する(implements)ためのクラス。拡張したものは、インスタンス化して使用する。

上記では、Test実装クラスではA, Cの2つのメソッドしか持たない。Test実装クラスをインスタンス化しているので、のこインスタンスが持つのはA, Cの2つのメソッド。
staticメソッドは、インターフェースとセットで実行。defaultメソッドは、実装クラスのインスタンスで実行。
インターでーすで定義したデフォルトメソッドは、実装クラスに受け継がれていることがわかる。

インターフェースの実装と同時に他クラスの継承

  • extendsが先。それ以外はコンパイルエラー。
interface Inter { void methodA(); }
class Super { void methodB(){} }
class Sub extends Super implements Inter { public void methodA(){} }

インターフェースの継承

  • extendsを用いてインターフェースを継承する。
  • 具象クラス、抽象クラスとは違って、インターフェースの場合、複数のインターフェースを継承することができる。
  • サブインターフェースを実装したクラスが具象クラスの場合、スーバーインターフェース、サブインターフェースの抽象メソッドをオーバーライドする必要がある。
Traning.java
//インターフェース1
interface Inter1 {
    void methodA();
}

//インターフェース2
interface Inter2 {
    void methodB();
}

//インターフェース3(複数継承)
interface Inter3 extends Inter1, Inter2{
    void methodC();
}

//設計図クラス
class Test implements Inter3 {
    public void methodA(){ System.out.println("methodA"); }
    public void methodB(){ System.out.println("methodB"); }
    public void methodC(){ System.out.println("methodC"); }
}

//実行クラス
class Training {
    public static void main(String[] args){
        Test t = new Test();
        t.methodA();
        t.methodB();
        t.methodC();
    }
}
//出力結果:
//methodA
//methodB
//methodC