ポリモーフィズム

復習

継承

パトカー is-a 自動車 のように「A is-a B」のような関係にあるクラスは、継承によってクラス B のメンバをクラス A に組み込ませることができた。

// 親クラス
public class A {
  // 親クラスのメンバ
}
// 子クラス
public class B extends A {
  // 子クラスのメンバ
}

上記のように書くと、A が持つメンバは B に組みこまれるため、B のインスタンスから A の公開メンバにアクセスすることができるようになる。

継承を用いるとクラスの拡張が容易になる。前回の例では、「自動車」というクラスを継承した「パトカー」というクラスは、サイレンを鳴らすというメソッドを宣言するだけで、自動車としての機能とサイレンを鳴らす機能を持つ「パトカー」を作成することができた。


コンストラクタの連鎖

コンストラクタは、クラスの継承によって親クラスから子クラスに直接組み込まれることはない。ただし、子クラスのコンストラクタを呼び出すと、必ず親クラスのコンストラクタが先に処理される必要があった。


親クラスの引数を持つコンストラクタを呼び出すには、super という命令を使用した。

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

    public Person(String name, int age) {
        // Person インスタンスのフィールドを設定
        ...
    }
    ...
}
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 命令は必ずコンストラクタの先頭に置かれる必要がある。また、この super 命令をコンストラクタ内に書かなかった場合、自動で super(); という命令がコンストラクタの先頭に追加される (引数無しの親コンストラクタが自動的に呼び出される)。

継承と型

子クラスのインスタンスを、親クラスの型を持つ変数に代入することができた。

public class A {
  public void aMethod() { System.out.println("a"); }
}
public class B extends A {
  public void bMethod() { System.out.println("b"); }
}
A inst = new B();
inst.aMethod();

上記の例では、子クラスを new してインスタンスを生成し、それを親クラスの型を持つ変数 inst に代入している。inst は A クラスの型を持つので B クラスのインスタンスメソッド (bMethod) は呼び出すことができないが、A クラスのインスタンスメソッドは呼び出すことができる (B is-a A なので、B は A としても振る舞える)。

メソッドの上書き

次の「人間」クラスについて考えてみる。

package j2.lesson05;

public class Person {
    
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
    
    public void introduceMyself() {
        System.out.println(getName() + "です。");
    }
}

このクラスは、public なメンバとして、getName() と introduceMyself() がある。前者はこの人の名前を取得するメソッドで、後者はこの人に自己紹介を行ってもらうメソッドである。

「人間」を継承した「学生」というクラスを作成する。

package j2.lesson05;

public class Student extends Person {

    private final String id;
    
    public Student(String name, String id) {
        super(name);
        this.id = id;
    }

    public String getId() {
        return this.id;
    }
    
    public void introduceMyselfAsStudent() {
        System.out.println(getName() + "です。");
        System.out.println("学籍番号は" + getId() + "です。");
    }
}

「学生」というインスタンスは、学籍番号を取得するメソッドと学生として自己紹介をするメソッドを持つ。通常の自己紹介との違いは、名前だけでなく学籍番号も一緒に表示している点である。

しかし、現実世界では「学生として自己紹介をしなさい」というような指示はあまりしない。通常は単に「自己紹介しなさい」という。introduceMyself() と introduceMyselfAsStudent() が同時に存在するのは分かりにくいし、そもそも introduceMyselfAsStudent() という名前は長すぎる。

Java では、親クラスで宣言したメソッドを子クラスで上書きすることができる。上書きのしかたは簡単で、子クラスで親クラスと同じ名前、同じ引数を持ったメソッドを宣言するだけである。

package j2.lesson05;

public class Student extends Person {

    private final String id;
    
    public Student(String name, String id) {
        super(name);
        this.id = id;
    }

    public String getId() {
        return this.id;
    }
    
    public void introduceMyself() {
        System.out.println(getName() + "です。");
        System.out.println("学籍番号は" + getId() + "です。");
    }
}

上記の例では、Person で宣言した introduceMyself() を Student で上書きしている。getName() は Student クラスには存在していないが、Person クラスを継承しているため使用することができる。

子クラスでメソッドを上書きすると、呼び出し時には子クラスのメソッドが呼び出される。

Student student = new Student("ほげほげ", "00k0000");
student.introduceMyself();
ほげほげです。
学籍番号は00k0000です。


また、他の子クラスを作成する際にも同じことができる。

package j2.lesson05;

public class Professor extends Person {
    
    private final String office;

    public Professor(String name, String office) {
        super(name);
        this.office = office;
    }

    public String getOffice() {
        return this.office;
    }

    public void introduceMyself() {
        System.out.println(getName() + "です。");
        System.out.println("オフィスは" + getOffice() + "です。");
    }
}

こちらも Person クラスで宣言した introduceMyself() を上書きしている。Student 同様、以下のようなプログラムが書ける。

Professor prof = new Professor("ふーばー", "W4999");
prof.introduceMyself();
ふーばーです。
オフィスはW4999です。

上記のように、親クラスのメソッドを上書きすることをメソッドのオーバーライドと呼ぶ (オーバーロードではないことに注意)。メソッドをオーバーライドすると、親クラスと同じ名前のメソッドを宣言することができ、クラスを使う側が覚えるべきことが少なくてすむ。

親クラスのメンバ呼び出し

先ほどの例で挙げた3つのクラスの introduceMyself() メソッドについて見直してみる。

public class Person {
    ...
    public void introduceMyself() {
        System.out.println(getName() + "です。");
    }
}
public class Student extends Person {
    ...
    public void introduceMyself() {
        System.out.println(getName() + "です。");
        System.out.println("学籍番号は" + getId() + "です。");
    }
}
public class Professor extends Person {
    ...
    public void introduceMyself() {
        System.out.println(getName() + "です。");
        System.out.println("オフィスは" + getOffice() + "です。");
    }
}

全てのメソッドで、1行目が「System.out.println(getName() + "です。");」である。プログラムで同じコードを何回も書くことは一般的にあまり好かれない。なぜなら、一つを修正すべきときに、同じコードを全て修正する必要があるからである。

上記の場合は、Student クラスと Professor クラスで、Person クラスの introduceMyself() メソッドを使用したい。しかし、それぞれの子クラスでメソッドを上書きしているため、単純に introduceMyself() メソッドを呼び出すと自分自身を呼び出すことになってしまう。

そこで Java では、super というキーワードを使用すると、親クラスが持つメソッドを呼び出すことができる。これは子クラスで上書きされていても関係なく、親クラスのメソッドが呼び出される。

public class Student extends Person {
    ...
    public void introduceMyself() {
        super.introduceMyself();
        System.out.println("学籍番号は" + getId() + "です。");
    }
}

このように書くと、親クラス (Person) の introduceMyself() メソッドが呼び出される。


この例で、例えば自分の名前の紹介方法に変更があったとする。そのような場合も親クラスのメソッドを呼び出すことによって、親クラスを一つだけ修正すれば Student や Person クラスを変更することなく、プログラム全体が修正される。

ちなみに、プログラムを書く際に念頭においておくべき原則として、DRY (Don't Repeat Yourself, 同じコードを繰り返すな) というものがある。自分が書いたプログラム内で重複している部分があったら、できるだけ違うメソッドに追い出してそれを呼び出すようにするとよい。上記の例では、親クラスのメソッドを呼び出すことによって重複を追い出した。

ポリモーフィズム

前回の講義で、is-a 関係にあるインスタンスは親クラスの型を持つ変数に代入できることを紹介した。

Person student = new Student("ほげほげ", "00k0000");
Person prof = new Professor("ふーばー", "W4999");

ここで、それぞれの introduceMyself() メソッドを呼び出すとどうなるか。


オブジェクト指向言語では、プログラムを書いている人がメソッドを呼び出すのではなく、「メッセージを送ってインスタンスにメソッドを呼び出してもらう」というイメージが近い。そのため、メソッドは次のように実行される。


実際にプログラムを書いてみると、次のようになる。

Person person = new Student("ほげほげ", "00k0000");
person.introduceMyself();
ほげほげです。
学籍番号は00k0000です。

このように、同じメッセージを送って、メッセージを受け取ったインスタンスがそのクラスによって違う振る舞いをすることを、ポリモーフィズム (多態性, 多様性, 多相性) と呼ぶ。上記の例では、introduceMyself() を受け取ったのが「学生」であるので、「学生としての自己紹介」が行われる。

上記の例では、person という変数に直接 Student クラスのインスタンスを生成して代入しているため、person.introduceMyself() メソッドが何をするか (どのクラスのインスタンスか) 予想が付く。しかし、メソッドの引数に Person 型の値を渡せる場面では、Professor や Student のインスタンスなどをメソッドの呼び出し元は指定することができるため、呼び出し先ではどのクラスのインスタンスが渡されてきたか判別できないが、introduceMyself() というメソッドを呼び出すことができる。

オブジェクト指向のポリモーフィズムによって、オブジェクトの実態が何であるか (Student, Professor, Person) を気にすることなく、単に Person の機能を持つ何かにメッセージを送ることにより、多態性のあるプログラムを一種類のプログラム片によって記述することができるようになる。使う側は実態が何であるかを気にすることなく、単純にその機能の名前を指定するだけで、機能に合った振る舞いをインスタンスに期待できる。

これは、現実世界でもよくあることである。例えば「(運転手つきの)乗り物」というものを考えた場合、目的地を指定することで乗客を目的地まで連れて行ってくれる。

これを、Javaのプログラムで記述すると次のようになる。

package j2.lesson05;

public class Vehicle {

    public Vehicle() {
        super();
    }

    public void go(String destination) {
        // 目的地へ向かう処理
    }
}

乗り物には様々な種類があり、「目的地へ行く」という振る舞いを一様に規定することができない。もう少し具体化したクラスを考えてみる。

例えば、タクシーは次のようにかけるかもしれない。

package j2.lesson05;

public class Taxi extends Vehicle {

    public Taxi() {
        super();
    }

    public void go(String destination) {
        this.accelerate();
        // destination へ向かう処理
        this.brake();
    }
    
    private void accelerate() {
        // アクセルを踏む処理
    }
    
    private void brake() {
        // ブレーキを踏む処理
    }
    
    private void turnLeft() {
        // 左折する処理
    }
    
    private void turnRight() {
        // 右折する処理
    }
}

他にも、乗り物としては空路が考えられる。空路を選ぶ際にも、行き先を指定してヘリコプターや飛行機に乗り込むことになる。

package j2.lesson05;

public class Helicopter extends Vehicle {

    public Helicopter() {
        super();
    }

    public void go(String destination) {
        this.takeOff();
        // destination へ向かう処理
        this.land();
    }

    private void takeOff() {
        // 離陸する処理
    }
    
    private void turn() {
        // 方向を制御する処理
    }
    
    private void land() {
        // 着陸する処理
    }
}

上記のように、クラスを使う側はどのような乗り物であっても関係なく、以下のように書くことができる。

Vehicle vehicle = ...;
vehicle.go("東京駅");

この際、go("東京駅") というメッセージを受け取ったインスタンスがタクシーであれば陸路を自動車として進むことになるし、ヘリコプターであれば空路を飛んで進むことになる。

使う側からすれば go("東京駅") という命令が実行されればどのような方法をとっても問題ない場合、単に Vehicle という型を持つインスタンスに対して go と指定すればよい。結果として、東京駅へ行くことができる。

その点、ポリモーフィズムの考え方を使って、親クラスに一種類のメッセージ (メソッド) だけを宣言して、子クラスでそのメッセージに対して処理を書けば (メソッドをオーバーライドすれば)、それぞれの子クラスを使用する際にも一種類のメッセージだけを覚えておけばすむ。

toString() の例

ポリモーフィズムを実感できる例として、toString() メソッドがある。

クラスの継承関係をたどっていくと、全てのクラスの親クラスとして java.lang.Object クラスに行き着く。つまり、全てのクラスは ~ is-a Object の関係が成り立つ。このクラスには toString() という String を返すメソッドが宣言されていて、このメソッドを呼び出すことによってそのインスタンスの文字列表現を取得することができる。

しかし、toString() というメソッドは子クラスでオーバーライドして、そのクラスごとの動作を規定しないと意味の分からない文字列を返す。

Object person = new Person("ほげほげ");
System.out.println(person.toString());
j2.lesson05.Person@1df5a8f

上記はプログラムを実行した結果で、j2.lesson05.Personクラスの toString() メソッドを呼び出したことは推測できるが、その中身がなにであるかは想像できない。

ここで意味のある文字列を表現させたい場合、toString() メソッドを Person クラスでオーバーライドする必要がある。

package j2.lesson05;

public class Person {
    ...
    public String toString() {
        return "人間:" + getName();
    }
}

これだけで、先ほどのプログラムの実行結果は変化する。

人間:ほげほげ

また、toString() というメソッドはさまざまなところで使用される。例えば、先ほどの例ではわざわざ toString() メソッドを呼び出して文字列に変換した後、その文字列を表示していた。

System.out.println(person.toString());

System.out.println メソッドには、Object 型をとるメソッドが用意されている (オーバーライドされている)。このメソッドを呼び出すと、引数に渡されたオブジェクトに対して自動的に toString() メソッドを呼び出した後、その文字列を表示する。つまり、そのままオブジェクトを渡しても同じ動作をしてくれる。

Object person = new Person("ほげほげ");
System.out.println(person);

こちらのほうが分かりやすい。なぜなら toString() メソッドを呼び出したプログラム System.out.println(person.toString()) では、「person の文字列化したものを表示する」という文章に変換できるが、そのまま System.out.println(person) と書くと「person を表示する」というように訳すことができる。

コンソールに文字列を表示する際には、当然のように文字列を表示する。そのため、わざわざ toString() を自分で呼び出して変換すると「コンソールに文字列を表示するメソッドに、引数にpersonを文字列に変換して文字列を表示する」というわけの分からない解釈になってしまう。それよりも単純に person というインスタンスを渡して、それをコンソールに文字列として表示してもらうほうが分かりやすい。

他にも、文字列の足し算を行う際に、自動的に toString() メソッドが呼び出される。

Object person = new Person("ほげほげ");
String str = "文字列に変換 -> " + person;
System.out.println(str);

こちらも、わざわざ "文字列に変換 -> " + person.toString() と書く必要はない。

アクセス制御

アクセス制御子をこれまでにいくつか紹介したが、メソッドのオーバーライドや継承などを含めて、いくつか追加する。

protected

public, private, そのどちらもつけない (パッケージアクセス)、というメンバの公開レベルを制御するアクセス制御子を紹介してきたが、他にも protected というものが存在する。

protected は public の次に公開性の高いキーワードで、同一パッケージ内または子クラスからアクセス可能という性質を持っている。例えば、全てに公開したくないが子クラス内では使用できるメソッドやフィールドを作成する場合は protected を指定するとよい。

キーワード クラス内 パッケージ内 子クラス内 その他
public
protected ×
なし × ×
private × × ×

クラス、メソッドのfinal

フィールドにメソッドをつけると、そのフィールドに特定の場面以外で代入ができなくなった。クラスやフィールドに final をつけると、継承を行う際にいくつか制限がかかる。

クラスに対して final を指定すると、そのクラスを継承することが禁止される (子クラスを作れなくなる)。Java に標準で組み込まれているクラスを調べてみると、System クラスや String クラスが public final class として宣言されている。

public final class String {
    ...
}

メソッドに対して final を指定した場合、そのメソッドをオーバーライドすることが禁止される。クラスを継承することは許しても、そのメソッドを上書きされると困るような場面で使用される。Java に標準で組み込まれているクラスでは、Object クラスの getClass() という、自分のクラスについての情報を得るためのメソッドがこの final で宣言されている。

public final Class getClass() {
    ...
}

確かに、自分のクラスを騙られると困る場面はあるが、Object クラス自体に final を指定されて継承を禁止されると、Java で全てのクラスを作れなくなる (Java のクラスは暗黙のうちに java.lang.Object を継承する)。