カテゴリー
Java

Java(Step10-2)

クラスの派生と多相性

多相性

メソッドのオーバーライド

次のプログラムを見てみましょう。このプログラムには、2つのクラスが定義されており、クラスPetとRobotPetです。

//Pet.java
//ペットクラス
public class Pet {
    private String name;
    private String masterName;
    //コンストラクタ
    public Pet(String name, String masterName){
        this.name = name;
        this.masterName = masterName;
    }

    //ペット名を調べる
    public String getName(){return name;}
    //持ち主名を調べる
    public String getMasterName(){return masterName;}

    //自己紹介
    public void introduce(){
        System.out.println("■私の名前は" + name + "です。");
        System.out.println("■持ち主は" + masterName + "です。");
    }
}

class RobotPet extends Pet{
    //コンストラクタ
    public RobotPet(String name, String masterName){
        super(name, masterName); //スーパークラスのコンストラクタ
    }

    //自己紹介
    public void introduce(){
        System.out.println("◇私の名前は" + getName() + "です。");
        System.out.println("◇持ち主は" + getMasterName() + "です。");
    }

    //家事を行う
    public void work(int sw){
        switch(sw){
            case 0: System.out.println("掃除します。"); break;
            case 1: System.out.println("洗濯します。"); break;
            case 2: System.out.println("炊事します。"); break;
        }
    }
}

■クラスPet

フィールド

  • name…ペットの名前
  • mastername…主人の名前

コンストラクタ

  • Pet…ペットと主人の名前を設定

メソッド

  • getName…ペットの名前を調べるメソッド
  • getMasterName…主人の名前を調べるメソッド
  • introduce…自己紹介するメソッド

■クラスRobotPet

フィールド

クラスPetのフィールドを継承しています。

コンストラクタ

  • RobotPet…ペットと主人の名前を設定します。設定作業は、super(…)を呼び出すことによって、スーパークラスPetのコンストラクタに任せます。

メソッド

  • introduce…自己紹介メソッド(クラスPetのものを上書きしています)
  • work…家事をするメソッド。家事の種類は、引数でも0,1,2の値として指定します。

メソッドintroduceのように、スーパークラスのメソッドと同形式のメソッドに、サブクラスで別の定義を与える事を「オーバーライドする(override)」と表現します。

多相性

上記2つのクラスをテストするプログラムを以下に示します。

//PetTester.java
//ペットクラスの利用例

class PetTester {
    public static void main(String[]args){
        Pet tarou = new Pet("Tarou", "あなた");
        tarou.introduce();
        System.out.println();

        RobotPet r2d2 = new RobotPet("R2D2", "ジェダイ");
        r2d2.introduce();
        System.out.println();

        Pet p  =r2d2;
        p.introduce();
    }    
}
PetTester.java実行結果
  1. クラスPetのインスタンスTarouを生成して、自己紹介を行わせます。呼び出されるのは、クラスPetに所属するメソッドintroduceです。
  2. クラスRobotPetのインスタンスr2d2を生成して、自己紹介を行わせます。呼び出されるのは、クラスRobotPetに所属するメソッドintroduceです。
  3. Pet型の変数pが、RobotPet型のインスタンスを参照するように初期化しています。

ここで、メソッド呼び出しp.introduce()に着目しましょう。このメソッド呼び出しには、以下のA方式とB方式のどちらに解釈されるでしょうか?

A.ペットPet用の自己紹介メソッドが呼び出される。

変数pの型がPetであるため、Pet型の自己紹介用メソッドintroduceが呼び出される。

B.ロボット型ペットRobotPet用の自己紹介メソッドが呼び出される。

参照先のインスタンスがRobotPet型であるため、RobotPet型の自己紹介用メソッドintroduceが呼び出される。

A方式では、呼び出すメソッドがコンパイル時に決定し、B方式では、呼び出すメソッドが実行時に決定します。実際に生成されるコードはB方式です。実行結果からもわかると思います。

クラス型変数が、派生関係にある様々なクラス型のインスタンスを参照できることを、多相性=ポリモーフィズム(polymorphism)と呼びます。

多相性が絡んだメソッド呼び出しでは、プログラム実行時に呼び出すメソッドが決定されます。そのメリットは、以下の通りです。

  • 異なるクラス型のインスタンスに対して同一のメッセージを送れる
  • メッセージを受け取ったインスタンスは自分自身の型が何であるかを知っており、適切な行動を起こす。

A方式は、呼び出すメソッドをコンパイル時に決定できるため、その呼び出しメカニズムは、静的結合(static binding)や早期結合(early binding)と呼ばれます。一方で、Javaで採用されているB方式は、呼び出すメソッドが実行時に決定されるため、その呼び出しメカニズムは動的結合(dynamic binding)や遅延結合(late binding)と呼ばれます。

動的結合をメソッドの引数に応用したプログラム例を以下に示します。

//PetTester2.java
//ペットクラスの利用例

class PetTester2 {
    //pが参照するインスタンスに自己紹介させる
    static void intro(Pet p){
        p.introduce();
    }    

    public static void main(String[] args){
        Pet[] a = {
            new Pet("Tarou", "あなた"),
            new RobotPet("R2D2", "ジェダイ"),
            new Pet("T-800", "未来のあなた"),
        };

        for(Pet p : a){
            intro(p);  //pが参照するインスタンスに自己紹介させる
            System.out.println();
        }
    }
}
PetTester2.java実行結果

オブジェクト指向の3大要素

ここまでで、継承と多相性について説明していきましたが、以下の3つはオブジェクト指向の3大要素と呼びます。

  • クラス
  • 継承
  • 多相性

参照型のキャスト

引き続き、クラスPetとクラスRobotPetを考えます。以下に示すように、スーパークラス型の変数はサブクラスのインスタンスを参照できます。

Pet p = new RobotPet("R2D2", "ジェダイ");

この時、RobotPet型への参照が、Pet型への参照へと暗黙の内にキャストされています。ここで行われる型変換は、参照型の拡大変換(widening reference conversion)またはアップキャスト(up cast)と呼ばれます。

一方、サブクラス型の変数はスーパークラスのインスタンスを参照できません。以下のようにキャスト演算子を明示的に適用すれば、型変換は可能になります。

Pet p = (Pet)new RobotPet("R2D2", "ジェダイ");

ここで行われる型変換は、参照型の縮小変換(narrowing reference conversion)または、ダウンキャスト(down cast)と呼ばれます。

instanceof 演算子

クラス型の変数は、そのクラス型のインスタンスだけでなく、上位クラスのインスタンスを参照することも、下位クラスのインスタンスを参照することもできます。そうすると、変数が参照しているのがどのクラスか調べる事も必要になります。それを行うプログラム例を以下に示します。

//PetInstanceOf.java
//instanceof演算子の利用例

class PetInstanceOf {
    public static void main(String[] args){
        Pet[] a = {
            new Pet("Tarou", "あなた"),
            new RobotPet("R2D2", "ジェダイ"),
            new Pet("T-800", "未来のあなた"),
        };

        for(int i = 0; i < a.length; i++){
            System.out.println("a[" + i + "]");
            if(a[i] instanceof RobotPet)
                ((RobotPet)a[i]).work(0);
            else
                a[i].introduce();
        }
    }
}
PetInstanceOf.java実行結果

初登場のinstanceof演算子を演算子を利用しています。これは、以下の形式で利用する関係演算子の1種です。

クラス型変数名 instanceof クラス名

x instanceof t変数xが型tに暗黙の内にキャストできる下位クラスであればtrueを、そうでなければ、falseを生成。
instanceof演算子

@Override アナテイション

以下に示すプログラム部分を見てください。クラスRobotPetと自己紹介用メソッドの部分です。introduceとすべきメソッド名を、introductionに間違えています。

class RobotPet {
    //中略
    public void introduction(){
        System.out.println("◇私の名前は" + getName() + "です。");
        System.out.println("◇持ち主は" + getMasterName() + "です。");
    }
    //中略
}

コンパイラでは、メソッドintroductionが新規に宣言されているとみなされてしまいます。そのため、スーパークラスで定義されたintroduceはそのまま継承され、定義されたintroductionは新しく追加されたメソッドとして扱われます。

このようなミスを防ぐのに有効なのが、アナテイション(annotation)です。プログラムの読み手に伝えるものをコメントとして記述しますが、アナテイションは、人にもコンパイラにも伝える注釈になります。

使い方は簡単で、以下のようにメソッド宣言の名前に前に @override と付けるだけです。

class RobotPet {
    //中略
    @Override public void introduction(){
        System.out.println("◇私の名前は" + getName() + "です。");
        System.out.println("◇持ち主は" + getMasterName() + "です。");
    }
    //中略
}

このアナテイションは、以下のことを示します。

「これから宣言するのは、上位クラスのメソッドをオーバーライドするメソッドです。本クラスで新しく追加するメソッドではないですよ。」

この場合、スーパークラスPetにメソッドintroductionが無いため、コンパイラはエラーを吐きます。これで、ミスに気づき、以下のようにプログラムの修正ができます。

class RobotPet {
    //中略
    @Override public void introduce(){
        System.out.println("◇私の名前は" + getName() + "です。");
        System.out.println("◇持ち主は" + getMasterName() + "です。");
    }
    //中略
}

継承とアクセス性

クラスの派生において、フィールドやメソッドは継承されるが、コンストラクタは継承されません。それらを明確にしていきたいと思います。

メンバ

クラスの派生で継承されるのは、クラスのメンバ(member)に限られます。以下にメンバを示します。

  • フィールド
  • メソッド
  • クラス
  • インタフェース

スーパークラスのメンバは、原則としてそのまま継承されます。ただし、非公開アクセス性をもつメンバ、privateで宣言されたメンバは継承されません。

メンバでない、クラスの資産には以下のものがあります。

  • インスタンス初期化子
  • 静的初期化子
  • コンストラクタ

これらは継承されません。

finalなクラスとメソッド

final付きで宣言されたクラスやメンバは、派生において特別な扱いになります。

finalクラス

finalクラスから派生を行うことはできません。つまり、finalクラスをスーパークラスとして作ることはできません。たとえば、文字列を表すStringクラスは、finalクラスになります。以下のように、Stringクラスを拡張したクラスを作ることはできません。

class DeluxeString extends String{  //エラー
   // ……
}

これは、以下の原則を示しています。

拡張すべきでないクラスは、finalクラスとして宣言せよ。

finalメソッド

finalメソッドは、サブクラスでオーバーライドすることができません。以下のようにメソッドの先頭にfinalを付けて宣言します。

final void f(){/* …… */}

オーバーライドとメソッドのアクセス性

メソッドをオーバーライドする際は、アクセス性と関連する以下の規則を必ず知っておかなければなりません。

アクセス制限の強さ、弱さの関係を示したのが以下の図になります。

メソッドのアクセス制限

下位クラスで上位クラスのものよりも強いアクセス制限の修飾子を与えることができません。その規則をまとめたのが以下の表です。

A/B公開限定公開パッケージ非公開
公開×××
限定公開××
パッケージ×
非公開××××
アクセス制限のリスト

Javaの全ての親玉であるObjectクラスは、toStringがpublicメソッドとして定義されています。toStringクラスをオーバーライドする際は、必ずpublicを付ける必要があります。