ポリモーフィズムとは
前までの記事で扱ってきたSalarymanクラスはHumanクラスを継承していました。
当然、Humanクラスの機能をすべて持っています。
そのため、Salarymanクラスで新たに定義したメンバや、Salarymanクラスであることを無視してHumanクラスのオブジェクトとして扱うこともできます。
例えば、以下のような書き方ができます。
Human taro = new Salaryman( );
これは「SalarymanのインスタンスをHumanとして扱う」ということです。
newが呼んでいるのはSalarymanのコンストラクタなので、メモリ上に存在するのはあくまでもSalarymanです。
しかし、そのうちSalarymanの要素は見ずに、Humanのオブジェクトとして扱うことができます。
このように、SalarymanクラスのインスタンスはSalarymanクラスのオブジェクトとして働くこともでき、Humanクラスのオブジェクトとしても働くことができます。
また、Objectクラスのオブジェクトとしても働くことができます。
このように、オブジェクトはさまざまなクラスのオブジェクトとして働きます。
これをオブジェクトの「ポリモーフィズム」あるいは「多様性」、「多態性」と言います。
ポリモーフィズムを使うことで、より柔軟にクラスを扱うことができるようになります。
どういった場合にポリモーフィズムを使うと便利なのかは、次のプログラムを作成しながら確認しましょう。
ポリモーフィズムの利用
Humanクラス
public class Human { private String name; private int age; private double height; public Human( ){ System.out.println("Humanの引数なしコンストラクタが呼ばれました!"); } public Human(String n, int a, double h){ this( ); System.out.println("Humanの引数ありコンストラクタが呼ばれました!"); name = n; age = a; height = h; } public void setName(String n){ name = n; } public void setAge(int a){ age = a; } public void setHeight(double h){ height = h; } public String getName( ){ return name; } public int getAge( ){ return age; } public double getHeight( ){ return height; } public void selfIntroduce( ){ System.out.println("私の名前は" + name + "です"); System.out.println("私の年齢は" + age + "です"); System.out.println("私の身長は" + height + "です"); } }
Slarymanクラス
public class Salaryman extends Human{ private int salary; private int bonus; public Salaryman( ){ System.out.println("Salarymanの引数なしコンストラクタが呼ばれました!"); } public Salaryman(String n, int a, double h, int s, int b){ super(n, a, h); System.out.println("Salarymanの引数ありコンストラクタが呼ばれました!"); salary = s; bonus = b; } public void selfIntroduce( ){ System.out.println("私の名前は" + getName( ) + "です"); System.out.println("私の年齢は" + getAge( ) + "です"); System.out.println("私の身長は" + getHeight( ) + "です"); System.out.println("私の年収は" + getIncome( ) + "です"); } public int getIncome( ){ return salary * 12 + bonus * 2; } }
UseHumanクラス
public class UseHuman{ public static void main(String[ ] args){ //Humanの配列に Human memberlist[ ] = new Human[10]; //Humanを継承したオブジェクトなら順不同で入れられる memberlist[0] = new Salaryman("太郎", 30, 175.5, 30, 60); memberlist[1] = new Human("次郎", 28, 177.3); memberlist[2] = new Salaryman("三郎", 25, 172.4, 20, 40); memberlist[3] = new Human("四郎", 22, 176.1); for(int i = 0; i < memberlist.length; i++){ //ここでは10個要素があるうち //データが入っているのは4個目まで if(memberlist[i] != null){ //読みやすくするため改行 System.out.println( ); memberlist[i].selfIntroduce( ); } } } }
実行結果
Humanの引数なしコンストラクタが呼ばれました! Humanの引数ありコンストラクタが呼ばれました! Salarymanの引数ありコンストラクタが呼ばれました! Humanの引数なしコンストラクタが呼ばれました! Humanの引数ありコンストラクタが呼ばれました! Humanの引数なしコンストラクタが呼ばれました! Humanの引数ありコンストラクタが呼ばれました! Salarymanの引数ありコンストラクタが呼ばれました! Humanの引数なしコンストラクタが呼ばれました! Humanの引数ありコンストラクタが呼ばれました! 私の名前は太郎です 私の年齢は30です 私の身長は175.5です 私の年収は480です 私の名前は次郎です 私の年齢は28です 私の身長は177.3です 私の名前は三郎です 私の年齢は25です 私の身長は172.4です 私の年収は320です 私の名前は四郎です 私の年齢は22です 私の身長は176.1です
UseHumanクラスの7行目から10行目でインスタンスを作成していますが、HumanのものとSalarymanのものが順不同で入り混じっています。
これをすべてHumanの配列でひとまとめにして扱うことができるのです。
これが、ポリモーフィズムの1番のメリットと言えるでしょう。
オブジェクト指向の新しい概念として重要なものは、カプセル化、継承、そしてポリモーフィズムの3つだということがよく言われます。
オブジェクトのキャスト
ポリモーフィズムはオブジェクトのキャストで実現できます。前項で出てきた
Human taro = new Salaryman( );
の形はキャストを省略していますが、もう少し細かく書くと
Human taro; Salaryman obj = new Salaryman( ); taro = (Human)obj;
ということを行っています。
実際はSalarymanのインスタンスを作成しても、キャストすることで他のクラスのオブジェクトとして扱えます(もちろん、継承していない、そのクラスの機能を持っていない場合はできません)。
しかし、キャストをしたことで使わない部分、Salarymanクラスで定義した情報は失われないのでしょうか?
これは大丈夫です。
Salarymanのインスタンスを生成し、キャストをしてもその情報はなくなりません。
これは基本データ型の変数とはまったく違いますので注意してください。
理由は、オブジェクトの中にはデータをそのまま入れるわけではないからです。
基本データ型の変数はそのままデータを入れているので、int型の変数に入っているものをshort型の変数に代入しようとするとデータが失われてしまいます。
ところが、オブジェクトのキャストは矢印の付け替えに過ぎません。
インスタンスそのものはまったく変更しません。
そのため、オブジェクトをスーパークラスにキャストしても、元のクラスにキャストすれば元通りSalarymanのクラスとして扱うことができます。
ポリモーフィズムとキャスト
public class UseHuman{ public static void main(String[ ] args){ Human memberlist[ ] = new Human[10]; //Humanを継承したオブジェクトなら順不同で入れられる memberlist[0] = new Salaryman("太郎", 30, 175.5, 30, 60); memberlist[1] = new Human("次郎", 28, 177.3); memberlist[2] = new Salaryman("三郎", 25, 172.4, 20, 40); memberlist[3] = new Human("四郎", 22, 176.1); //元通り取り出すことができる Salaryman taro = (Salaryman)memberlist[0]; taro.selfIntroduce( ); } }
実行結果
Humanの引数なしコンストラクタが呼ばれました! Humanの引数ありコンストラクタが呼ばれました! Salarymanの引数ありコンストラクタが呼ばれました! Humanの引数なしコンストラクタが呼ばれました! Humanの引数ありコンストラクタが呼ばれました! Humanの引数なしコンストラクタが呼ばれました! Humanの引数ありコンストラクタが呼ばれました! Salarymanの引数ありコンストラクタが呼ばれました! Humanの引数なしコンストラクタが呼ばれました! Humanの引数ありコンストラクタが呼ばれました! 私の名前は太郎です 私の年齢は30です 私の身長は175.5です 私の年収は480です
オブジェクトをスーパークラスに多様させるときは自動的にキャストされますが、元に戻すには明示的にキャストをしなければなりません。
もし、
Salaryman taro = memberlist[0];
のように、キャストを行わないとコンパイルエラーになります。
すべてのSalarymanはHumanの要素をもちますが、すべてのHumanがSalarymanの要素を持っているとは限らないからです。
メソッドの動的結合
ポリモーフィズムを使ったときに気をつけなければいけないのは、どのクラスのメンバにアクセスするのか?ということです。
例えば、今までの例では
Human taro; taro = new Salaryman( );
などとしています。
宣言はHumanクラスですが、インスタンスを生成しているのはSalarymanクラスです。
このとき、taroがどちらのクラスとして扱われるかという問題があります。
下図の状況下では、taro.xとかtaro.selfIntroduce( )としています。
このとき、両方のクラスにxやselfIntroduce( )メソッドがあったときどちらにアクセスするのでしょうか?
宣言はHuman型ですが、インスタンスはSalarymanです。
どちらに結び付けられるかによって実行結果が変わってしまいます。(結び付けられることを結合、バインド、リンクなどと言います。)
結論としては、taro.xはHumanクラスのxにアクセスし、taro.selfIntroduce( )はSalarymanのselfIntroduce( )メソッドにアクセスします。
なぜこのようになるのでしょうか。
Javaでは以下のように扱われます。
フィールド
宣言時の型で結合(静的結合)メソッド
実行時の型で結合(動的結合)
静的、動的というのはプログラミングでよく用いられる言葉です。
静的というのはコンパイルしたときにすでに決まっており、動的というのはコンパイル時には決まらず、実行しているときにはじめて決まるということです。
フィールドの場合はHumanクラスで宣言されているのでHumanのxが使われます。
一方メソッドは、コンパイル時にはどのselfIntroduce( )メソッドを呼ぶのか決まりません。
実行時にオブジェクトtaroがメソッドを呼ぶと、実際にメモリ上にあるこのオブジェクトが何なのか(Humanクラスなのか、Salarymanクラスなのか)を調べ、それから合致するメソッドを呼びに行きます。
つまり自動的に判別してくれます。
まとめ
ポリモーフィズムとは
- ポリモーフィズムを使うことで、サブクラスをスーパークラスとして扱うことができます。
- ポリモーフィズムを使うことで、サブクラスをスーパークラスとして扱うことができます。
オブジェクトのキャスト
- ポリモーフィズムはオブジェクトのキャストと等しいです。
- オブジェクトはキャストしてもデータが失われることはありません。
メソッドの動的結合
- フィールドは静的結合、メソッドは動的結合します。
参考図書
独学で挫折しそうになったら、オンラインプログラミングスクール