カテゴリー
Java

Java(Step10-1-2)

クラスの派生と多相性

継承②

メソッドの上書きとsuperの正体

スーパークラスのコンストラクタを呼びだすための super の正体は、そのクラスに部分として含まれるスーパークラス部への参照です。そのため、次の規則があります。

super. メンバ名によって、スーパークラスのメンバをアクセスできる。

このことを、以下に示すプログラムで確認しましょう。

//SuperTester.java
//スーパークラスとサブクラス

//スーパークラス
class Base{
    protected int x; //限定公開

    Base(){this.x = 0;}
    Base(int x){this.x = x;}

    void print(){System.out.println("Base.x = " + x);}
}
//サブクラス
class Derived extends Base{
    int x; //スーパークラスと同一名のフィールド

    Derived(int x1, int x2){super.x = x1; this.x = x2;}
    //スーパークラスのメソッドを上書き(オーバーライド)
    void print(){super.print(); System.out.println("Derived.x = " + x);}
}

public class SuperTester {
    public static void main(String[] args){
        Base a = new Base(10);
        System.out.println("- a -"); a.print();
        Base b = new Derived(20, 30);
        System.out.println("- b -"); b.print();
    }    
}

■クラスBase

このクラスで宣言されたフィールドxは、protected付きで宣言されています。このように宣言されたメンバは、たとえパッケージの外であってもサブクラスからアクセスできる限定公開アクセスになります。

■クラスDerived

このクラスでは、フィールドxを宣言しています。クラスBaseのフィールドxと同名ですが、クラスBaseから継承したものとは別物で扱われます。

クラスの派生とsuper

コンストラクタに注目すると、super.xは、スーパークラスBaseから継承したフィールドxのことであり、this.xは、自分自身のクラスDerivedで宣言されたフィールドxのことです。

メソッドprintは、引数を受け取らず値を返却しないので、クラスBaseのものと同じ形式になります。このメソッド中のsuper.print()は、スーパークラスBaseに所属するメソッドprintの呼び出しです。

クラス階層

ここまでの例は、あるクラスから別のクラスを派生する例でしたが、派生したクラスからさらに派生を行うこともできます。その例が以下になります。

クラスの派生

クラスAからクラスBを派生し、クラスBからクラスC、Dへ派生しています。クラスBはAのです。そして、クラスCとDは、Bのであると同時に、Aのです。すべてのクラスに血縁関係があります。

親を含めた上側のクラスを祖先、子を含めた下側のクラスを子孫と呼ぶことにすると、上位クラス・下位クラスは以下のように定義されます。

  • 上位クラス(super class)…祖先クラス
  • 下位クラス(sub class)…子孫クラス
  • 直接上位クラス(direct super class)…親クラス
  • 直接下位クラス(direct sub class)…子クラス

より詳細に示した表も併せて以下に示します。

名称定義
スーパークラス派生の元になったクラス(親)
サブクラス派生によって作られたクラス(子)
上位クラス親を含めた祖先クラス
下位クラス子を含めた子孫クラス
間接上位クラス親を除いた祖先クラス
間接下位クラス子を除いた子孫クラス
各クラスの定義

Javaでは、複数のクラスから派生を行う多重継承がサポートされていません。そのため、以下に示すクラスはコンパイルエラーになります。

Javaは多重継承をサポートしない

Objectクラス

以下の図を見てみましょう。この図では、Objectというクラスが登場します。クラスAのように、extendsを付けずに宣言されたクラスは、Objectクラスのサブクラスになります。

クラスの派生

したがって、今まで作成してきたextendsを伴わないクラスは、objectクラスのサブクラスだったということです。

差分プログラミング

銀行口座クラスに対し、定期預金を追加する例を別のクラスとして作り直すのではなく、派生によってクラスを作ることにしましょう。それが以下のクラスです。

//TimeAccount2.java
class TimeAccount2 extends Account2 {
    private long timeBalance;

    TimeAccount2(String name, String no, long balance, long timeBalance){
        super(name, no, balance);
        this.timeBalance = timeBalance;
    }

    long getTimeBalance(){
        return timeBalance;
    }

    void cancel(){
        deposit(timeBalance);
        timeBalance = 0;
    }
}

本クラスでは、フィールド・コンストラクタ・2つのメソッドのみを宣言します。それ以外のフィールドとメソッドは、スーパークラスAccount2から継承しているので、新たに宣言する必要がありません。

継承のメリットの1つは、「既存プログラムに対する必要最低限の追加。修正だけで新しいプログラムが完成する」という差分プログラミング(incremental programming)が行われることです。プログラム開発時の効率アップや保守性の向上が図れます。

is-Aの関係とインスタンスへの参照

TimeAccount2は、Account2の子供であって、Account2家に属していると考えられます。このことはis-Aの関係と呼ばれ、以下のように表現します。

TimeAccount2は、一種のAccount2である。

この関係の逆は成立しないことに注意しましょう。この関係をプログラム上で活用した例を以下に示します。

//TimeAccount2Tester.java
//is-Aの関係とインスタンスへの参照
class TimeAccount2Tester{
    public static void main(String[]args){
        Account2 tarou = new Account2("山田太郎", "12345", 1000);
        TimeAccount2 hanako = new TimeAccount2("山田花子", "67890", 200, 500);

        Account2 x;
        x = tarou; //自分自身の型のインスタンス参照可
        x = hanako; //下位クラス型のインスタンス参照可

        System.out.println("xの預金残高:" + x.getBalance());

        TimeAccount2 y;
        //y = tarou; //上位クラス型のインスタンス参照不可
        y = hanako; //自分自身の型のインスタンス参照可

        System.out.println("yの預金残高:" + y.getBalance());
        System.out.println("yの定期預金残高" + y.getTimeBalance());
    }
}
TimeAccount2Tester.java実行結果

mainメソッドの冒頭で、以下に示す2つのインスタンスを生成しています。

  • tarou…銀行口座クラスAccount2型のインスタンス
  • hanako…定期預金付き銀行口座クラスTimeAccount2型のインスタンス

その後で宣言されている変数xは、Account2型のクラス型変数で、変数yはTimeAccount型のクラス型変数です。

次に考えるのは、以下のプログラムです。

//TimeAccount2Tester2.java
//is-Aの関係とインスタンスへの参照

class TimeAccount2Tester2 {
    //どちらの預金残高が多いか
    static int compBalance(Account2 a, Account2 b){
        if(a.getBalance() > b.getBalance()) //aの方が多い
            return 1;
        else if(a.getBalance() < b.getBalance()) //bの方が多い
            return -1;
        return 0; //aとbは同じ
    }
    
    public static void main(String[]args){
        Account2 tarou = new Account2("山田太郎", "12345", 1000);
        TimeAccount2 hanako = new TimeAccount2("山田花子", "56789", 200, 500);

        switch(compBalance(tarou, hanako)){
            case 0: System.out.println("二人の預金残高は同じ。"); break;
            case 1: System.out.println("太郎君の預金残高が多い。"); break;
            case -1: System.out.println("花子ちゃんの預金残高が多い。"); break;
        }
    }
}
TimeAccount2Tester2.java実行結果

仮引数aとbの型はAccount2型です。mainメソッドでは、それらに対して、Account2型インスタンスへの参照とTimeAccount2型への参照を渡しています。これが上手くいくのは、Account2型の変数が、Account2型のインスタンスと、TimeAccount2型インスタンスのいずれも参照できるためです。

※メソッドのクラス型引数に対しては、そのクラス型のインスタンスへの参照だけでなく、そのクラスの下位クラス型のインスタンスへの参照を渡すことができる。