継承

復習

パッケージ

「完全限定名」と「単純名」について学んだ。

単純名でアクセスできるクラスは、次のようなものがあった。

アクセス制御

他のクラスから、自分のクラスやインスタンスが持つメンバへのアクセスを制限することができた。

また、一度だけ書き込み可能という変数の宣言方法を学んだ。

カプセル化

内部の実装を隠蔽して、外部からのメソッド呼び出しによってのみ機能するように設計することを、カプセル化と呼んだ。

カプセル化を行う際には、基本的には全てのインスタンスフィールドを private で宣言するのが望ましい。インスタンスフィールドを変更する際は、変更のためのメソッドを用意して、それを用いて変更する。

これにより、フィールドに変更があった際などや、複数のフィールドを一貫性を持って同時に変更しなければならない場合に柔軟に対応できるようになる。

例えば、三角形を表すインスタンスの全ての辺の長さを2倍にする場合を考える。

public class Triangle {
    public double a, b, c;
}

上記の各辺を2倍する場合、次のようなプログラムを書いてしまうかもしれない。

Triangle t = new Triangle();
t.a = t.a * 2;
t.b = t.a * 2;
t.c = t.c * 2;

上記のプログラムにはバグがある (t.b の長さが不正になる)。もし、2倍するという操作がこの部分だけなら修正は容易であるが、様々なソースコード内にコピーが埋め込まれていた場合、修正は困難になる。

この場合、「辺の長さをn倍にする」というメソッドを用意して、そのメソッドを介してのみ「辺の長さをn倍にする」という動作を行えるようにする。

public class Triangle {
    private double a, b, c;
    public void scale(double n) {
        this.a = this.a * n;
        this.b = this.b * n;
        this.c = this.c * n;
    }
}

このように書いておけば、使う側も「t.scale(2.0)」のようにするだけでよいし、修正を行う場合もこの部分だけを修正すればよい。また、三角形の内部表現が「三辺の長さ」から「二辺とその狭角」で表すことになっても、修正は容易である。

継承

オブジェクト指向言語の強力な機能に継承という概念がある。同時に、オブジェクト指向の考え方で最も難しいものの一つであるが、とりあえずはイメージをつかんでほしい。

まず、自動車というものについて特徴を抽出し、Javaで表現してみる。

詳細の実装までは書かないが、クラスの形は以下のようになる。

package j2.lesson04;

public class Car {

    private double speedMeter;

    public void accelerate() {
        // 加速する処理
        ...
    }
    
    public void brake() {
        // 減速する処理
        ...
    }
    
    public void turn() {
        // 進行方向を変える処理
        ...
    }
    
    public void shiftGear() {
        // ギアをチェンジする処理
        ...
    }
}

次に、パトカーというものの特徴を考えてみると、以下のようなものが挙げられる。

これを Car 同様に Java のクラスで表現すると、以下のようになる。

package j2.lesson04;

public class PoliceCar {
    
    private Siren siren;
    
    private double speedMeter;
    
    public void accelerate() {
        // 加速する処理
    }
    
    public void brake() {
        // 減速する処理
    }
    
    public void turn() {
        // 進行方向を変える処理
    }
    
    public void shiftGear() {
        // ギアをチェンジする処理
    }
    
    // サイレンを鳴らす
    public void soundSiren() {
        this.siren.sound();
    }
}

上記の PoliceCar クラスは Car と重複部分の多いプログラムになってしまった。これは、パトカー自体が自動車であるので、パトカーが自動車の特徴を有しているためである。

パトカーの特徴をもう一度見ると、下線部は自動車の特徴であった

もし、パトカーについて詳しくない人に「パトカーの特徴」について問われた場合、上記のように説明することはあまりない。通常は以下のように行うはずである。

このように、パトカーはそもそも自動車であって、それにいくつか特徴を付け加えたものである。


このように「元となるものがあり、それに特徴を付け加える」というような考え方をオブジェクト指向では継承と呼ぶ。継承はクラス単位で行い、継承の元となるクラスを「親クラス (スーパークラス)」と呼び、それを継承したものを「子クラス (サブクラス)」と呼ぶ。

オブジェクト指向言語で継承を行うと、親クラスのメンバ (メソッドとフィールド) が子クラスに組み込まれ、子クラスのメンバと同様に子クラスのインスタンスから使用できるようになる。

ただし、継承によって組み込まれるメンバに private が指定されている場合、子クラスからそのメンバにアクセスすることは出来ない。private はあくまで「宣言したクラス内から使用可能」というだけであって、そのクラスを継承した子クラスはあくまで「そのメンバを宣言したクラスとは別のクラス」として扱われる。

Java でクラスの継承を行いたい場合、クラスの宣言時に以下のように書く。

public class SubClass extends SuperClass

このように書くことによって、SubClass は SuperClass を継承することになり、SuperClass の各メンバが SubClass に組み込まれる。例えば、パトカーが自動車を継承している場合、「PoliceCar extends Car」と書ける。これは日本語に直したときに「パトカーは自動車を拡張する」であり、"パトカーが自動車の特徴 (メンバ) を継承した上に、独自の特徴を付け加えて拡張している" という意味になる。

以上を踏まえると、先ほど出てきた「パトカーを表すクラス」は継承を利用して以下のように簡単に書くことができるようになる。

package j2.lesson04;

public class PoliceCar2 extends Car {

    private Siren siren;
    
    // サイレンを鳴らす
    public void soundSiren() {
        this.siren.sound();
    }
}

上記のように、パトカーの「自動車に付け加えた特徴」だけを書くことになるので、非常にプログラムがきれいになる。しかも、機能は先ほど出てきた長い「パトカーを表すクラス (class PoliceCar)」と同じものが使える (継承によってPoliceCar2へ組み込まれている)。

また、パトカーではなく自動車を継承した「救急車」などを作成する場合も、「自動車」を継承して救急車特有の特徴をプログラムするだけでクラスが作成できるようになり、生産性も向上する。


継承したクラスの使い方

継承の簡単な例として、「学生」と「教授」を表すクラスを作成する。

継承を使わない場合、Student クラスと Professor クラスをそれぞれ別々に作ればよかったが、それではプログラムに無駄な部分が多くなってしまう。まず、Student と Professor に共通する事項を考える。学生も教授も「人間」であるので、まずは「Person クラス」を作成する。

(これらの例では、フィールドの継承を確認するためにインスタンスフィールドを public で指定しているが、実際のプログラムではカプセル化の考え方に基づき private で宣言すべきである。)

public class Person {
    // 名前
    public String name;

    // 年齢
    public int age;

    // 自己紹介を行う
    public void introduceMyself() {
        System.out.println(name + " (" + age + ")");
    }
}

この Person クラスを親クラスとして、Student クラスと Professor クラスを作成する。

public class Student extends Person {

    // 学籍番号
    public String studentId;

    // 学年
    public int grade;
}
public class Professor extends Person {

    // 教員番号
    public String professorId;

    // オフィスの部屋名
    public String office;
}

すると、以下の図のように Student クラスと Professor クラスのインスタンスが作成できるようになる。


実際に Java からこのクラスを扱う方法を示す。

// Student の例
Student m99k9999 = new Student();
m99k9999.name = "ほげほげ";
m99k9999.age = 18;
m99k9999.studentId = "99K9999";
m99k9999.grade = 1;
// Professor の例
Professor prof = new Professor();
prof.name = "ふーばー";
prof.age = 50;
prof.professorId = "20050916";
prof.office = "W4999";

上記のように、インスタンスを作成する場合は今までどおり「new クラス名()」を指定する。Student クラスや Professor クラスでは name, age というフィールドを明示的に用意してはいないものの、extends Person と書いて Person クラスを継承したことによって、Person クラスのフィールドを Student や Professor のインスタンスからアクセスできている。

上記の例ではインスタンスフィールドへのアクセスしか行っていないが、インスタンスメソッドへのアクセスも可能である。

// Student の例 (メソッド)
Student m99k9999 = new Student();
m99k9999.name = "ほげほげ";
m99k9999.age = 18;
m99k9999.introduceMyself();

つまり、Person で public で宣言したメンバは、Person クラスを継承した Student, Professor クラスのインスタンスから、Student, Professor クラスのメンバであるかのように使用することができる。

コンストラクタの流れ

ここまでの説明で、「継承するとフィールドとメソッドが子クラスに組み込まれる」という話をしたが、コンストラクタについては触れてこなかった。

コンストラクタはクラスと言う設計図を元にインスタンスという製品を製造する工場のようなもので、これは継承によって親クラスから子クラスに組み込まれることはない。Car クラスのコンストラクタで PoliceCar クラスのインスタンスを生成することはできないからである。

そのため、コンストラクタは継承を行っても親クラスと子クラスで別々に用意する必要がある。

public class Car {

    // Car インスタンスを生成するコンストラクタ
    public Car() {
        // ...
    }
}
public class PoliceCar extends Car {

    // PoliceCar インスタンスを生成するコンストラクタ
    public PoliceCar() {
        // ...
    }
}

ただし、Java では子クラスのコンストラクタを呼び出すと、自動的に親クラスのコンストラクタが先に実行されるというルールがある。


子クラスのコンストラクタから親クラスのコンストラクタを呼び出すには、コンストラクタの先頭部分で「super();」という親クラスのコンストラクタを呼び出す命令を使用する。

public class PoliceCar extends Car {

    // PoliceCar インスタンスを生成するコンストラクタ
    public PoliceCar() {
        // Car クラスのコンストラクタ Car() を呼び出す
        super();

        // ...
    }
}

これまでは super() という書き方をしてこなかったが、これは省略可能で省略時には自動的にコンストラクタの先頭でsuper()が実行されるという規則がある。そのため、何もしなくても親クラスのコンストラクタは自動的に実行される。

super() は引数なしの親コンストラクタを呼び出す書き方で、括弧の中に親クラスのコンストラクタに渡すための引数を書くことによって、引数ありの親コンストラクタを呼び出すこともできる。

public class Person {
    public String name;
    public int age;

    public Person(String name, int age) {
        // Person インスタンスのフィールドを設定
        this.name = name;
        this.age = age;
    }
    ...
}
public class Student extends Person {
    public String studentId;
    public int grade;
    
    public Student(String name, int age, String id, int grade) {
        // Person(String, int) を呼ぶ
        super(name, age);
        
        // Student インスタンスのフィールドを設定
        this.studentId = id;
        this.grade = grade;
    }
}

注意すべき点は、super による親クラスのコンストラクタ呼び出しは、必ずコンストラクタの先頭で1回だけしか行うことができない。そのため、次のようなプログラムはエラーになってしまう。

public Student(String name, int age, String id, int grade) {
    this.studentId = id;
    this.grade = grade;

    // Person(String, int) を呼ぶ -> 途中からは呼べない
    super(name, age);
}

親クラスと子クラスの関係

これまでに例として紹介した以下のクラスは「子クラス」と「親クラス」の関係にあった。

これらは、次のような is-a 関係 と呼ばれる関係で言い表すことができる。

このように、「A is-a B」で表されるようなクラス A, B は

public class A extends B

というような親子関係を持つクラスで表すことができる。

ここで、「A is-a B」が成り立つとき、必ずしも「B is-a A」が成り立つとは限らないことに注意しよう。

そのため「A is-a B」であるときには、Bが親クラスでAが子クラスである。

ところで、PoliceCar クラスは「class PoliceCar extends Car」と、Car が親クラスであることを明示的に書いていた。それに対して Car クラスは「class Car」と親クラスを指定しなかった。

Java では、明示的に親クラスを extends を指定しないことによって、暗黙のうちに java.lang.Object というクラスを継承することになる。

なぜこのようなことをするか、というのはそれなりに難しい問題であるが、これから先の講義を通して少しずつ説明していく予定である。

継承と型

ここ数年で、携帯電話にカメラが搭載された機種がかなりの数を占めてきた。それについて考えてみる。

このように、日常では「機能が拡張された製品の、元の機能を使用する」ということを無意識に行っている。

カメラ付き携帯電話はあくまでも携帯電話として使用できる。そのため、次のように使用できるはずである。

// カメラ付き携帯電話をインスタンス化する
CameraCellPhone camphone = new CameraCellPhone();

// 普通の携帯電話として扱う
CellPhone phone = camphone;

// 普通の携帯電話として電話する
phone.call("090xxxxyyyy");

実際、Java ではこのようにすることが可能である。つまり、子クラスのインスタンスを親クラスのインスタンスの型として扱うことができる

ただし、CameraCellPhone のインスタンスを CellPhone として扱う (型をCellPhoneにする) 場合、カメラ付き携帯電話の特徴を使用することはできない。つまり、CameraCellPhone インスタンスに特有の shoot() というメソッドが存在したとしても、そのメソッドを CellPhone の型から使用することはできない。

また、PoliceCar is-a Car の関係でも同様にプログラムが書ける。

// パトカーをインスタンス化して、自動車として扱う
Car car = new PoliceCar();

// 自動車として操作する
car.accelerate();

自動車を使用したい場合、その自動車がどのような車種であっても普通は問題ない (パトカーは少し問題があるが)。ただし、自動車を扱うための普通自動車免許は必要で、これさえあれば普通の「自動車」は運転することが出来るようになる。

一般に、自動車を運転する場合は特定の車種に限定した免許というものを必要としない。その代わり「第一種普通自動車免許」というものを取得していれば、いわゆる「自動車」というものを運転することが出来る。例えば、その自動車がタクシーであったとしても、通常の自動車として利用する (旅客運送をしないで回送する) 場合はこの免許で運転することが出来るし、パトカーも簡単な検定試験をパスすれば運転できるようである。

これは、Taxi is-a Car であり、PoliceCar is-a Car であるため、子クラス特有の機能を意識せずに Car クラスとして扱えば、必要な機能を使用することが出来るためである。

このような考え方をすると、プログラムに対して Taxi を操作する場合の処理、PoliceCar を操作する場合の処理、とそれぞれを分けて書く必要がなくなり、単純に自動車を操作する場合の処理というものを一つだけ書いてやることにより、この処理を Taxi にも PoliceCar にも適用できるようになる。

次の例では、人間に対して「引数で渡された自動車を運転する」というメソッドを定義している。

public class Person {
    
    // 車を運転するメソッド
    public void drive(Car car) {
        // ...
    }
}
public class Student extends Person {
    // ...
}

上記のクラスを用いて、簡単な例を示す。

Student student = new Student();
PoliceCar policecar = new PoliceCar();

// 学生がパトカーを運転する
student.drive(policecar);

上記のプログラム例は「学生がパトカーを運転する」というものであるが、実際には「ただの人間 (Person) がただの自動車 (Car) を運転する」という書き方をすることによって、よりプログラムを一般的なものにしている。例えば、この Student が Professor でも同じようにプログラムを書けるし、PoliceCar が Taxi (is-a Car) でも同じようにプログラムが書ける。

まとめ

少し今回は理解しにくい内容であったかもしれないが、出来るだけ単純に考えることが重要である。

過去の講義で紹介したように、インスタンスメソッドの呼び出しは「インスタンスにメッセージを送って、インスタンス自身がメソッドを起動する」という流れで行われる。

例えば PoliceCar のインスタンスに accelerate() というメッセージを送った場合、まずは自分 (PoliceCar インスタンス) の中に accelerate() というメソッドがどこにあるかを探す。今回の例では PoliceCar が Car を継承していて、その Car の中で accelerate() というメソッドが宣言されていた。

PoliceCar は Car を継承しているため、継承によって PoliceCar インスタンスには accelerate() というメソッドも組み込まれている。メッセージを受け取ったインスタンスは、最終的に accelerate() という Car クラスで宣言されたメソッドを起動することになる。

この「メッセージ」という考え方は、次回で特に重要になってくる。不安があれば復習して身に着けておくこと。