コンピュータ基礎および演習II

講義資料

継承

オブジェクト指向概念の下では、 同じような性質を備えたオブジェクトをクラスとして扱います。

例えば「時計」というクラスを考えてみます。 クラス「時計」には時計全般に共通する性質が定義されます。 しかし、腕時計と置き時計では時計に共通する性質は同じですが、 細部の性質には違いがあります。 このような場合に、 「腕時計」と「置き時計」のクラスで 時計としての性質を全て定義するのではなく、 あらかじめ「時計」というクラスを定義しておき、 腕時計と置き時計にその性質を渡すのが良いと言えます。

このように、同じような性質を備えたものを一つのクラスにまとめておくと、 プログラムの記述に重複がなくなるので、 プログラムが単純になり、すっきりした設計になります。

以上のように、オブジェクト指向プログラミング言語では、 既存のクラスを拡張して新しいクラス定義することができます。 このような概念を、継承 (inheritance) と言います。

継承の関係は何階層にも渡ることができます。 「時計」→「置き時計」→「目覚まし時計」などのように、 何段階にも継承を考えることができます。

あるクラスを継承して別のクラスを定義した場合、 継承元のクラスをスーパクラスと呼び、 継承されたクラスをサブクラスと呼びます。 スーパクラスは親クラス、上位クラス、基底クラスなどと呼ばれることもあり、 また、サブクラスは子クラス、下位クラス、派生クラスなどと呼ばれることもあります。

クラスを継承すると、 スーパクラスの属性やメソッドはほぼ全てサブクラスに受け継がれます。 ただし、コンストラクタは受け継がれません。 コンストラクタはクラスごとに宣言する必要があるということに注意が必要です。

例題1: 円を継承し表示可能な円を定義する

前々回の演習問題の問題1で作成した、 面積と円周長を計算するクラス Circle を拡張して、 円の図形を表示する機能を追加したクラス PrintableCircle を定義することを考えてみます。 (ファイル名 ExtendedCircle.java)

このような場合、これまで学んだやり方では、 新たに一からクラスを定義しなおさなければなりません。 Circle の定義はそのまま利用し、 追加機能だけを書くことができれば便利です。 このようなときに継承を用いるのです。

class Circle {
    protected int radius;

    public Circle(int r) {
        radius = r;
    }

    public Circle() {
        radius = 1;
    }

    public int area() {
        return radius * radius * 3;
    }

    public int outline() {
        return 2 * radius * 3;
    }
}

class PrintableCircle extends Circle {

    public PrintableCircle(int r) {
        radius = r;
    }

    public void print() {
        // (-10,-10) から (10,10) の矩形内の点が円内にあるかチェック
        for(int y = 10; y >= -10; y--) {
            for(int x = -10; x <= 10; x++) 
                if(x*x + y*y <= radius*radius)
                    System.out.print("**");
                else
                    System.out.print("  ");
            System.out.println();
        }
    }
}

class ExtendedCircle {
    public static void main(String[] args) {
        PrintableCircle c1 = new PrintableCircle(10);

        System.out.println("Area: " + c1.area());
        System.out.println("Outline: " + c1.outline());
        c1.print();

    }
}

最初のクラス Circle の定義では、 これまで同様に円のクラスを定義しています。

次のクラス PrintableCircle の定義では、 Circle を継承しこのクラスのサブクラスとして定義しています。

このように、あるクラスを継承してサブクラスを定義する場合、 文法的には次のように書きます。

class  サブクラス名  extends  スーパクラス名  {
    ......
    クラス定義の内容
}

サブクラスでは、 スーパクラスの属性とメソッドがほぼすべて受け継がれ、 そのまま使用することができます。 「クラス定義の内容」では、 スーパクラスの機能を拡張するために、 通常のクラス定義と同様に属性の宣言やメソッドの宣言を行うことができます。
ただし、 コンストラクタはスーパクラスから受け継がれません。 コンストラクタはクラス独自に宣言する必要があります。

クラス PrintableCircle では メソッド print のみを宣言していますが、 このインスタンスからは、 クラス Circle の持つメソッド areaoutline を呼び出すことができます。 クラス PrintableCircle は、 クラス Circle 性質を受け継いでいるからです。

クラス PrintableCircle はクラス Circle のプログラムを再利用できたということです。 継承によるプログラムの再利用は、 すでに完成されたクラスを、 クラスの中身を改造することなく機能を拡張する場合に有効です。 スーパクラスが目的の仕事をしてくれると信用しブラックボックスとみなし、 追加したい機能にだけ集中することができます。 また、他人が作ったクラスを継承して、 自分に必要な機能を追加するということも可能なのです。

修飾子 protected

クラスのメンバへアクセスを制限するための修飾子に public, protected, private があるということを説明しましたね。 public はどのクラスからもアクセスできるもっともオープンなメンバ、 private はそのクラス内からしかアクセスすることができないメンバでした。

protected のメンバは、 そのクラスの中と、 そのクラスを継承したサブクラスからのみアクセス可能となります。 クラスとそのサブクラスに対してのみ公開し、 外部のクラスに対しては非公開としたいメンバは、 protected にします。

この例題では、 PrintableCircleprint メソッドで Circle の属性の radius を参照するため、 クラス Circle の属性 radius は protected として宣言しています。

本講義では詳しくは扱いませんが、 さらに、そのクラスのファイルが存在する同じディレクトリ内にあるクラスに対しては、 サブクラスではなくてもメンバにアクセス可能です。 変則的な規則に思えるかも知れませんが、 これは、「同じパッケージ内では公開される」という決まりなのです。

例題2: TDUStudent を継承し、I科生、C科生のクラスを定義する

11月10日の例題で示した電大生を表すクラス TDUStudent を継承し、 I科生を表すクラス IMStudent と C科生を表すクラス CStudent を定義します。 (ファイル名 ThreeStudents.java)

class TDUStudent {
    protected int id;
    protected String name;
    protected int math;
    protected int english;
    protected int physics;

    public TDUStudent(int i, String n) {
        id = i;
        name = n;
    }

    public void setScore(int m, int e, int p) {
        math = m;
        english = e;
        physics = p;
    }

    public String getName() {
        return name;
    }

    public int average() {
        return (math + english + physics) / 3;
    }
}

class IMStudent extends TDUStudent {
    private int mediaKiso;
    private int designKiso;

    public IMStudent(int i, String n) {
        super(i, n);
    }

    public void setScore(int m, int e, int p, int media, int design) {
        math = m;
        english = e;
        physics = p;
        mediaKiso = media;
        designKiso = design;
    }

    public int average() {
        return (math + english + physics + mediaKiso + designKiso ) / 5;
    }
}

class CStudent extends TDUStudent {
    private int eleKiso;
    private int magKiso;

    public CStudent(int i, String n) {
        super(i, n);
    }

    public void setScore(int m, int e, int p, int ele, int mag) {
        math = m;
        english = e;
        physics = p;
        eleKiso = ele;
        magKiso = mag;
    }

    public int average() {
        return (math + english + physics + eleKiso + magKiso ) / 5;
    }
}

class ThreeStudents {
    public static void main(String[] args) {
        IMStudent john = new IMStudent(1, "John Lennon");
        CStudent paul = new CStudent(2, "Paul McCartney");
        TDUStudent george = new TDUStudent(3, "George Harrison");

        john.setScore(80, 90, 100, 90, 80);
        paul.setScore(90, 70, 60, 70, 60);
        george.setScore(80, 90, 100);

        TDUStudent[] students = new TDUStudent[3];
        students[0] = john;
        students[1] = paul;
        students[2] = george;

        for (int i = 0; i < students.length; i++)
            System.out.println(students[i].getName() + ": " +
                               students[i].average());
    }
}

クラス IMStudent に注目してみます。 これは、クラス TDUStudent を継承し、 新たな属性 int mediaKisoint designKiso を定義し、 メソッド setScore, average を定義しています。

メンバの上書き

メソッド setScore は、 スーパクラスにも存在するメソッドです。 しかし、スーパクラスのメソッド setScore と引数の数が異なります。 このようにシグネチャが異なると、 同じ setScore であっても、 別のメソッドが追加されたものとして扱われます。 (前々回オーバロードについて説明したとおりです。)

クラス IMStudent のインスタンスに対して、 メソッド setScore を int 型の引数 3 つを用いて実行すると、 英語、数学、物理の点数が登録され、 メソッド setScore を int 型の引数 5 つを用いて実行すると、 英語、数学、物理、メディアの基礎、デザインの基礎の点数が 登録されるということです。

継承では、スーパクラスに属性やメソッドを追加するだけではありません。 スーパクラスの属性やメソッドを上書きし、 新たなものと置き換えることができます。 これを、オーバライド (override) と言います。

メソッド avetage はスーパクラスにも存在し、 返り値が int 型であり引数が無いという点でも同じメソッドです。 このようにシグネチャが同じメソッドを定義すれば、 サブクラスであるクラス IMStudent で新しく宣言されたものと 置き換えることが可能です。

クラス IMStudent のインスタンスに対して、 メソッド average を実行すると、 スーパクラスの 3 科目の平均点を求める計算ではなく、 5 科目の平均点を求める計算が行われます。

キーワード super

サブクラスの中でオーバライドされたメソッドについて、 オーバライドされる前のスーパクラスの元のメソッドを実行する場合、 キーワード super を用います。

キーワード super を用いて、 スーパクラスの属性やメソッドにアクセスするには、 文法的には次のように書きます。

super.属性名
super.メソッド名 ( ... )

例えば、 クラス IMStudent のメソッド average の中の平均点の計算を、 3 科目の平均点を求めるスーパクラスのメソッド agerage の結果を用いて行う場合、 キーワード super を用いて、次のように書くことができます。

    int average() {
        int avgOfMediaDesign = (mediaKiso + designKiso) / 2;
        return super.average() * 3/5 + avgOfMediaDesign * 2/5;
    }

サブクラスのコンストラクタ

サブクラスがスーパクラスから受け継がないものにコンストラクタがあります。 したがって、サブクラスではコンストラクタを定義する必要があります。

ただし、スーパクラスのコンストラクタが使われないというわけではありません。 サブクラスのインスタンスが生成されると、 サブクラスのコンストラクタの内容が実行される直前に、 自動的にスーパクラスの引数のなしのコンストラクタ (デフォルトコンストラクタ) が実行されます。

次のようなクラスの継承を考えてみます。

class Parent {
    Parent() {
        // クラス Parent のデフォルトコンストラクタ
    }
    Parent(int a) {
        // クラス Parent の int 型引数ありのコンストラクタ
    }
}

class Child extends Parent {
    Child(int a) {
        // クラス Child の int 型引数ありのコンストラクタ
    }
}

ここで、new Child(100) のように、 クラス Child のインスタンスを生成した場合、 自動的にスーパクラス Parent の引数なしのデフォルトコンストラクタ (Parent()) が実行されます。

クラス Child のインスタンスが生成される際に、 スーパクラス Parent の int 型引数あり コンストラクタ (Parent(int a)) が実行されるようにするには、 キーワード super を用いて、 クラス Child のコンストラクタに次のよう書きます。

    Child(int a) {
        super(a);

        ......
        クラス Child のコンストラクタでの処理内容
    }

なお、 キーワード super を用いたスーパクラスのコンストラクタの実行は、 サブクラスのコンストラクタの一行目で一度だけ行うことができます。

実は、継承されないということと、 実際に実行されないということは別の概念なのです。

クラス IMStudent のコンストラクタでも同様に、 super(i, n) によって、 スーパクラス TDUStudent のコンストラクタを 適切な引数で呼び出し、 クラス TDUStudent の属性である学籍番号と名前を登録する処理を行うようにしているのです。

継承によるオブジェクトのグループ化

継承の利点はプログラムの再利用だけではありません。 継承のもう一つの目的は、 スーパクラスを起点としてサブクラス群をグループ化することです。 つまり、異なったクラスのインスタンスでも、 そのスーパクラスが同じであれば、 スーパクラスの一員であるとみなし、 同じように扱うことができるのです。

この例題である、スーパクラス TDUStudent を継承した クラス IMStudent, CStudent を考えます。

class TDUStudent {
    ......
}
class IMStudent extends TDUStudent {
    ......
}
class CStudent extends TDUStudent {
    ......
}

クラス IMStudent, CStudent のそれぞれのインスタンスは、 クラス TDUStudent の性質も持っているため、 クラス TDUStudent の型の変数に入れて使うことができます。

        IMStudent john = new IMStudent(1, "John Lennon");
        CStudent paul = new CStudent(2, "Paul McCartney");
        TDUStudent george = new TDUStudent(3, "George Harrison");

        TDUStudent[] students = new TDUStudent[3];

        students[0] = john;
        students[1] = paul;
        students[2] = george;

こうすることで、 john, paul, george の 3 つのインスタンスを TDUStudent の一員として 扱うことができるようになったのです。 スーパクラスのメソッドはサブクラスにも受け継がれていることから、 クラス IMStudentCStudent は、 平均点の計算方法は異なっても、 メソッド average によって平均点を計算する、 という同一のインタフェースを持っているということに注目しましょう。

例えば、次のように繰り返しを用いて、 異なるクラスのインスタンスに対するメソッドの実行を、 スーパクラスのインタフェースを用いて一括して行うことができるのです。

        for (int i = 0; i < students.length; i++)
            System.out.println(students[i].average());

このように継承によってクラスをグループ化するということは、 同じ型として扱えるようにするということと、 同じオブジェクトとして振る舞うための要素を持たせる、 すなわち、オブジェクトのインタフェースを揃えるという重要な意味があるのです。

メソッド average は、 クラス IMStudentCStudent で計算方法が異なっています。 インタフェースは同一で、クラスごとに異なった処理を行っているということに 注目しましょう。 このように、同じメッセージ (メソッドの実行) でも オブジェクトによって異なった振る舞いをするという概念を ポリモフィズムと言います。 継承はポリモフィズムを実現する一つの手段なのです。