Javaプログラミング基礎 講義資料

継承

今回のテーマでは、クラスの継承について取り上げます。 クラスは「拡張」することができます。 すでに存在するクラスを元にして、新しい属性やメソッドを追加したり、 すでにある属性やメソッドを上書きして新しいクラスを定義することができます。 例えば、「携帯電話」「カメラ付き携帯電話」「音楽再生機能付き携帯電話」 というクラスを考えてみます。 「カメラ付き携帯電話」、「音楽再生機能付き携帯電話」は、 「携帯電話」を拡張したものだと言えます。 このように、あらかじめ元となる「携帯電話」の機能を定義しておき 「カメラ付き携帯電話」 「音楽再生機能付き携帯電話」で さらに各携帯電話固有の機能を拡張する方法があります。

オブジェクト指向プログラミングでは、 まず元になるクラスを定義しておき、さらにこのクラスの機能を 引き継ぐ形で新たなクラスを作ることができます。 同じような性質を備えたものを一つのクラスの元にまとめておくと、 プログラムの記述に重複がなくなるので、 プログラムが単純になり、すっきりした設計になります。

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

継承の関係は何階層にも渡ることができます。 「電話」→「携帯電話」→「カメラ付き携帯電話」などのように、 何段階にも継承を考えることができます。

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

クラスを継承すると、一部の例外を除いて、 スーパクラスの属性やメソッドはほぼ全てサブクラスに受け継がれます。

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

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

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

public 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();

    }
}

// スーパクラス: 面積と円周長を求めることができる「円」クラス
class Circle {
    int radius;

    Circle(int r) {
        radius = r;
    }

    Circle() {
        radius = 1;
    }

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

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

// サブクラス: 「円」クラスを継承し、円の図形を表示する機能を追加したクラス
class PrintableCircle extends Circle {

    PrintableCircle(int r) {
        radius = r;
    }

    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();
        }
    }
}

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

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

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

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

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

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

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

繰り返しになりますが、 サブクラスでは、 スーパクラスの属性とメソッドがほぼすべて受け継がれ、 そのまま使用することができます。 サブクラスでは、拡張したい部分を書けば良いのです。 ただし、コンストラクタはスーパクラスから受け継がれません。

例題2: 電大生を継承し、FI科生、FR科生のクラスを定義する

電大生を表すクラス TDUStudent を考えてみます。 このクラスの属性には、学籍番号、氏名と、 数学、物理、英語のそれぞれの点数があるものとします。 また、平均点を計算するメソッドがあるものとします。

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

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

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

    String getName() {
        return name;
    }

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

この電大生を表わすクラス TDUStudent を継承し、 FI科生を表すクラス FIStudent と FR科生を表すクラス FRStudent を定義します。 (ファイル名 ThreeStudents.java)

クラス FIStudent には、 TDUStudent の属性に加えて、 「プログラミング基礎」、「CGプログラミング」 の点数があり、 この2科目を含めた合計5科目の平均点を計算するメソッド average があるとします。

また、クラス FRStudent には、 「ロボットの基礎」、「メカトロニクスの基礎」 の点数を入れる属性と、 この2科目を含めた合計5科目の平均点を計算するメソッド average があるとします。

public class ThreeStudents {
    public static void main(String[] args) {
        FIStudent john = new FIStudent(1, "John Lennon");
        FRStudent paul = new FRStudent(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);

        System.out.println(john.getName() + ": " + john.average());
        System.out.println(paul.getName() + ": " + paul.average());
        System.out.println(george.getName() + ": " + george.average());
    }
}


class TDUStudent {
    .... 
    上の定義のとおり
}

class FIStudent extends TDUStudent {
    int programming;
    int cg;

    FIStudent(int id, String name) {
        super(id, name);
    }

    void setScore(int mathScore, int englishScore, int physicsScore,
                  int programmingScore, int cgScore) {
        math = mathScore;
        english = englishScore;
        physics = physicsScore;
        programming = programmingScore;
        cg = cgScore;
    }

    int average() {
        return (math + english + physics + programming + cg) / 5;
    }
}

class FRStudent extends TDUStudent {
    int robot;
    int mechatoronics;

    FIStudent(int id, String name) {
        super(id, name);
    }

    void setScore(int mathScore, int englishScore, int physicsScore,
                  int robotScore, int mechatoronicsScore) {
        math = mathScore;
        english = englishScore;
        physics = physicsScore;
        robot = robotScore;
        mechatoronics = mechatoronicsScore;
    }

    int average() {
        return (math + english + physics + roboto + mechatoronics) / 5;
    }
}

メンバの上書き

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

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

クラス FIStudent のオブジェクトに対して、 メソッド setScore を int 型の引数 3 つを用いて実行すると、 英語、数学、物理の点数が登録され、 メソッド setScore を int 型の引数 5 つを用いて実行すると、 英語、数学、物理、プログラミング基礎、CG基礎の点数が 登録されるということです。

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

メソッド average に注目しましょう。 このメソッドは、スーパクラスにも存在し、 返り値が int 型であり引数が無いという点でも同じメソッドです。 このようにシグネチャが同じメソッドを定義すれば、 サブクラスであるクラス FIStudent で新しいものと 置き換える (上書きする) ことが可能です。

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

キーワード super

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

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

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

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

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

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

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

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

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

...

    Child myChild = new Child(10);    ← Parent(), Child(10)の順に実行される

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

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

    Child(int a) {
        super(a);    ← Parent(a)が実行される

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

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

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

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

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

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

class TDUStudent {
    ......
}
class FIStudent extends TDUStudent {
    ......
}
class FRStudent extends TDUStudent {
    ......
}

クラス FIStudent, FRStudent のそれぞれのオブジェクトは、 クラス TDUStudent の性質も持っているため、 クラス TDUStudent として宣言した変数に入れて使うことができます。

        FIStudent john = new FIStudent(1, "John Lennon");
        FRStudent paul = new FRtudent(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 の一員として 扱うことができるようになります。 スーパクラスのメソッドはサブクラスにも受け継がれていることから、 クラス FIStudentFRStudent は、 平均点の計算方法は異なっても、 メソッド average によって平均点を計算する、 という同一のインタフェースを持っているということに注目しましょう。

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

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

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

メソッド average は、 クラス FIStudentFRStudent で計算方法が異なっています。 インタフェースは同一で、クラスごとに異なった処理を行っているということに 注目しましょう。 このように、同じメッセージ (メソッドの実行) でも オブジェクトによって異なった振る舞いをすることができるのです。

継承に関係する修飾子

アクセスを制限する修飾子として、 前回 public, private の2つの修飾子を取り上げましたが、 継承を用いたクラスの場合必要に応じて protected を使うことができます。

protected

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

さらに、例外として、そのクラスのファイルが存在する 同じディレクトリ内にあるクラスに対しては、 サブクラスではなくてもメンバにアクセス可能です。