Javaプログラミング基礎

講義資料

継承

今回のテーマは、クラスとクラスの間の関係を扱います。 例えば、「携帯電話」「カメラ付き携帯電話」「音楽再生機能付き携帯電話」 というクラスを考えてみます。 これらのクラスの間には分類上の上下関係があると考えられます。 「カメラ付き携帯電話」 「音楽再生機能付き携帯電話」では、「携帯電話」としての性能は同じですが、 細部の性能に違いがあります。 このような場合に、 あらかじめ一般的な「携帯電話」の機能を定義しておき 「カメラ付き携帯電話」 「音楽再生機能付き携帯電話」で さらに各携帯電話固有の機能を全て定義する方法があります。

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

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

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

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

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

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

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

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

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 があるとします。

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

修飾子

今回取り上げる 2 つめの話題は修飾子です。 修飾子は、クラスの属性やメソッドを、 他のクラスからアクセスできるようにするかどうかや、 変数の書き換えを禁止させるかどうか といった様々な設定を行うためのものです。

クラスの利点の1つに、他のオブジェクトに対してクラス内部の細かい情報を 見せなくても、メソッドの実行の組み合わせでプログラムを書くことができる という点があります。 クラス内部の情報である属性は、外部オブジェクトから直接値を書き換えたり 参照したりせずに、公開されたメソッドを通じてのみ 変更や参照を行う方が好ましいのです。 これにより、意図しない属性の使われ方を防ぐことができます。

例えば、次のような人物を表わすクラスがあったとします。

class Person {
    String name;  // 名前
    int age;      // 年齢
}

やろうと思えば、このクラス Person のオブジェクトに不正な値を入れること ができてしまいます。

Person man = new Person();
man.age = -100;

上の例では、年齢に -100 を代入していますが、 年齢として許される値ではありません。 下のプログラムのようにメソッドを使い属性の値を登録すれば、 不正な値が入力されることによるプログラムの誤った動作を防ぐことができます。

class Person {
    String name;  // 名前
    int age;      // 年齢

    void setAge(int a) {
        if (a > 0 && a < 120)
            age = a;
        else
            System.out.println("不正な年齢の値です:" + a);
    }
}
Person man = new Person
man.setAge(20);

これまでは、メソッドを用いることで、 内部の属性に対して直接操作を行わないような プログラムの書き方をするように心掛けてきました。 しかし、属性を直接書き換えるような、 好ましくないプログラムも作ろうと思えば作れてしまいます。 Java には、 属性やメソッドへのアクセスを明示的に許可、禁止する方法があります。

Java では、クラスの属性とメソッドを宣言するときに、 それらを保護するために、 以下の 4 つのアクセスレベルを指定することができます。 private, protected, public そして指定なしのアクセスレベルです。

以下に、属性とメソッドに対する 4 つのアクセスレベルについて説明します。

private

もっとも厳しいアクセス制限レベルは private です。 private のメンバは、 そのクラスの中でのみアクセス可能となります。 外部からアクセスされたくない属性や、 外部から実行されたくないメソッドを private にします。

private のメンバを宣言するには、 宣言時にキーワード private を使用します。

前に示した理由から、一般に属性の値は外部から直接値を参照したり、 書き換えることを避けるべきです。 このような場合に private を使います。 次の例は、クラス Person の各属性を private にした場合の プログラムです。

class Person {
    private String name;  // 名前
    private int age;      // 年齢

    void setAge(int a) {
        if (a > 0 && a < 120)
            age = a;
        else
            System.out.println("不正な年齢の値です:" + a);
    }
}

属性だけでなく、メソッドも private にすることができます。 private メソッドは、外部のクラスからはアクセスできず、 そのクラスの中からのみ実行することができるようになります。 例えば、クラスの中の下請け処理を行うようなメソッドは、 private とし不用意に外部から使われないようにするのが好ましいのです。

protected

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

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

public

最もオープンなアクセスレベルは public です。 public メンバは外部に対して公開され、 任意のクラスからアクセスすることが可能です。 例えば、外部から実行されるメソッドは public にします。

public のメンバを宣言するには、 宣言時にキーワード public を使用します。

次の例は、クラス Person の各属性を private にし、 各メソッドを public にした場合のプログラムです。

class Person {
    private String name;  // 名前
    private int age;      // 年齢

    public void setName(String n) {
        name = n;
    }

    public void setAge(int a) {
        if (a > 0 && a < 120)
            age = a;
        else
            System.out.println("不正な年齢の値です:" + a);
    }
}

指定なし

これまでのプログラムでは、 main メソッドを除いて、 修飾子を特に指定しませんでした。

この場合、 そのクラスのファイルが存在する 同じディレクトリ内にあるすべてのクラスに対しては、 メンバにアクセス可能となります。 (これは「同じパッケージ内では公開される」という決まりなのですが、 パッケージについての詳しい内容は本講義では取り扱わないこととします。)

private, public, protected のキーワードは、 いずれか一つのみを指定することしかできません。

その他の修飾子

変数のその他の修飾子

クラスの属性や局所変数 (ローカル変数) の宣言時に指定することができる 修飾子に以下のものがあります。

final で指定された変数は、 宣言時に初期化し、それ以降値の変更ができないことを示します。

プログラムの中で一度決めたら途中で変更することがない定数として用いる場合、 キーワード final を用いて変数宣言を行います。 以下に、簡単な例を示します。

public Circle {
    // 円周率 PI を 3.14 に初期化
    private final double PI = 3.14;
    ...

    public void somethingToCalculate() {
        // 自然対数の底 E を 2.72 に初期化
        final double E = 2.72;
        ...
    }
}

なお、上の変数 PI の宣言のように、 キーワード final は private, public などの他の修飾子と組み合わせて使うことができます。

static については、後の回で扱います。

メソッドのその他の修飾子

private, public, protected に加えて、 メソッドに指定することができる修飾子に以下のものがあります。

static については、後の回で扱いますが、 その他の修飾子については本講義では扱いません。 皆さんの自習にまかせることにします。