カテゴリー
Java

Java(Step10-1-1)

クラスの派生と多相性

継承①

既存クラスの資産を継承しつつ、新しくクラスを作るための技術である、クラスの派生について説明していきます。

銀行口座クラス改良

以前、銀行口座クラスを作成しましたが、ここではさらに改良して「定期預金」を表せるように変更することを考えます。以下に示すフィールドとメソッドを追加しましょう。

  • 定期預金の残高を表すフィールド
  • 定期預金の残高を調べるメソッド
  • 定期預金を解約して全額を普通預金に移すメソッド

上記のフィールドとメソッドを追加したクラスを以下に示します。

//TimeAccount.java
//定期預金付き銀行口座クラス

class TimeAccount {
    private String name; //口座名義
    private String no;  //口座番号
    private long balance;  //預金残高
    private long timeBalance; //預金残高

    //コンストラクタ
    TimeAccount(String n, String num, long z, long timeBalance){
        name = n;
        no = num;
        balance = z;
        this.timeBalance = timeBalance;
    }
    
    //口座名義を調べる
    String getName(){
        return name;
    }

    //口座番号を調べる
    String getNo(){
        return no;
    }

    //預金残高を調べる
    long getBalance(){
        return balance;
    }

    //定期預金残高を調べる
    long getTimeBalance(){
        return timeBalance;
    }

    //k円預ける
    void deposit(long k){
        balance += k;
    }

    //k円おろす
    void withdraw(long k){
        balance -= k;
    }

    //定期預金を解約して全額普通預金に移す
    void cancel(){
        balance += timeBalance;
        timeBalance = 0;
    }
}

クラス TimeAccount は、「定期預金を扱う」という目的は満たせます。しかし、普通預金だけの銀行口座クラスとの互換性は失われます。そのことを以下に示すメソッドで考えましょう。これは、2つの口座の預金残高を比較して、その大小関係を整数値として返すメソッドです。

//どちらの預金残高が多いか
    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は同じ
    }

このメソッドに対して、TimeAccount 型インスタンスを渡すことは不可能です。クラス TimeAccount と Account2 クラスは別物だからです。

派生と継承

このような問題を解決するのが、クラスの派生(derive)です。派生とは、既存クラスのフィールドやメソッドなどの資産を継承(inheritance)した新しいクラスを作り出すことです。なお、派生の際は、資産を継承するだけでなくフィールドやメソッドを追加・上書きできます。

例えば、Baseというクラスがあって、その資産を継承したクラスの Derived を派生するイメージを下の図に表します。派生によって新しく作るクラスの宣言では、extends に続いて派生元のクラス名を書きます。

クラスの派生

派生元になるクラスと、派生によって作られたクラスのことを以下のように表現します。

  • 派生元のクラス…親クラス/上位クラス/基底クラス/スーパークラス
  • 派生したクラス…子クラス/下位クラス/派生クラス/サブクラス

各クラスの資産は次のようになっています。

■クラス Base

フィールドは、a、b の2個でメソッドは、a、bのセッタとゲッタの4個です。

■クラス Derived

このクラスで宣言しているのはフィールド c と、そのセッタとゲッタです。さらにクラス Baseのフィールドとメソッドを継承しているので、それらを合わせると、フィールドは3個、メソッドは6個になります。

サブクラスは、スーパークラスのフィールドやメソッドなどの資産を継承しますので、スーパークラスのメンバはサブクラス中に含まれます。そのため、スーパークラスとサブクラスの資産は図に示す関係になります。

派生による資産の継承とクラス階層図

派生とコンストラクタ

派生において継承されない資産があります。その1つがコンストラクタです。コンストラクタについては、クラスの派生と関連して、いくつかの点を必ず押さえる必要があります。まずは、派生を行う具体的なプログラムを見てみましょう。

//PointTester.java
//2次元座標クラスと3次元座標クラス

//2次元座標クラス
class Point2D{
    int x;
    int y;

    Point2D(int x, int y){this.x = x; this.y = y;}

    void setX(int x){this.x = x;}
    void setY(int y){this.y = y;}
    int getX(){return x;}
    int getY(){return y;}
}
//3次元座標クラス
class Point3D extends Point2D{
    int z;

    Point3D(int x, int y, int z){super(x, y); this.z = z;}

    void setZ(int z){this.z = z;}
    int getZ(){return z;}
}
public class PointTester {
    public static void main(String[] args){
        Point2D a = new Point2D(5, 10);
        Point3D b = new Point3D(15, 20 ,25);

        System.out.printf("a = (%d, %d)\n", a.getX(), a.getY());
        System.out.printf("b = (%d, %d, %d)\n", b.getX(), b.getY(), b.getZ());

    }
}

①スーパークラスのコンストラクタはsuper(…)によって呼び出せる。

このプログラムでは、2次元座標クラス Point2D、3次元座標クラス Point3D、それらをテストするクラスが定義されています。

  • クラス Point2D…2次元座標クラス(X座標/Y座標)

座標を表す2個のフィールドと、x、yと、4個のセッタ・ゲッタと、コンストラクタから構成されるクラスです。コンストラクタは仮引数x、yに受け取ったX座標とY座標の値を、フィールドxとyに設定します。

  • クラス Point3D…3次元座標クラス(X座標/Y座標/Z座標)

2次元座標クラスPoint2Dクラスから派生したクラスです。X座標とY座標に関するフィールドとメソッドは、そのまま継承します。新しく追加されたのは、Z座標のフィールドzと、そのセッタ、ゲッタです。

コンストラクタ内の super(x, y); に注目してみましょう。この式は、スーパークラスのコンストラクタの呼び出しになります。super(x, y)の呼び出しを行うのは、仮引数x、y に受け取った値をフィールドとxとyに代入する作業を、スーパークラスのコンストラクタに委ねるためです。その結果、クラスPoint3Dのコンストラクタ内で直接値を代入するのは、新たに追加されたZ座標用フィールドzだけですみます。ただし、super(…)の呼び出しは、コンストラクタの先頭でのみ行えます。

②サブクラスのコンストラクタ内では、スーパークラスに所属する「引数を受け取らないコンストラクタ」が自動的に呼び出される。

クラスPoint3Dのコンストラクタからsuper(…)の呼び出しを削除して、以下のように書きかえてみましょう。そうすると、コンパイルエラーが発生します。

//コンパイルエラー
Point3D(int x, int y, int z){this.x = x; this.y = y; this.z = z;}

このようなsuper(…)を明示的に呼び出さないコンストラクタには、スーパークラスに所属する「引数を受け取らないコンストラクタ」の呼び出し、すなわちsuper()の呼び出しがコンパイラによって自動的に挿入歌されます。以下のように書きかえられます。

//コンパイルエラー
Point3D(int x, int y, int z){super(); this.x = x; this.y = y; this.z = z;}

コンパイルエラーとなる理由は単純で、スーパークラスPoint2Dに「引数を受け取らないコンストラクタ」が存在せず、それを呼び出せないからです。

③コンストラクタを1つも定義しなければ、super()を呼び出すだけのデフォルトコンストラクタが自動的に定義される。

コンストラクタを1個も定義しないクラス X に対して、何も行わない空のコンストラクタであるデフォルトコンストラクタが、コンパイラによって以下の形式で自動的に定義されますと以前説明しました。

X(){}

そこでの解説は、厳密にいうと説明不足で、自動生成されるデフォルトコンストラクタは本当は以下のようになります。

X(){super();}  //コンパイラによって生成されるデフォルトコンストラクタ

このことを検証する具体的なプログラム例を以下に示します。

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

//スーパークラス
class A{
    private int a;
    A(){a = 50;}
    int getA(){return a;}
}
//サブクラス
class B extends A{
    //デフォルトコンストラクタが生成
}

public class DefaultConstructor {
    public static void main(String[] args){
        B x = new B();
        System.out.println("x.getA() = " + x.getA());
    }
}
DefaultConstructor.java実行結果

クラスAが持つ唯一のフィールドが、int型のaです。コンストラクタは、そのフィールドaに50を代入します。メソッドはgetAは、その値のゲッタです。クラスBは、クラスAのサブクラスです。コンストラクタが1個も定義されないため、デフォルトコンストラクタがコンパイラで定義されます。

注意点としては、クラスにコンストラクタを定義しない場合、そのスーパークラスが、「引数を受け取らないコンストラクタ」を持っていなければならない。