三大機能、ラストは「ポリモーフィズム」です!
近頃は、無駄話が少なかったので、今日は少し前置きを長くします。
「カラカル」ってご存知でしょうか?
(ご存知の方は、すみません。知らないつもりで!)
聞いたことない!!って方に、まず画像を。
これを見て、なんだと思うか?
椅子や、オムライスには見えませんね。
動物に見えるはずです。
動物は動物でも、何に似てるかなー??と考えていきつくのは
おそらく、ネコ!(トラもかな)あたりですよね。
今日、学ぶ、ポリモーフィズムというのは
このカラカルをネコと捉えるテクニックです。
カラカルを人に、「四本足でー」、「胎生でー(卵を産まない)」、「夜行性でー」と長々と説明するよりも「ネコ科の動物だよ!」と一言伝えたら、おおよその姿形、習性はわかりますよね。
「厳密には、カラカル!だけど、ネコとして捉えよう!」というのがポリモーフィズムです。
その、ポリモーフィズムの利用に特殊な構文はない。(今まで学んできた、extendsやprivateのような)
今日は、自分だけの動物園を作るために、動物を沢山作ってみます。
package zoo;
// 継承の材料用のアニマル(動物)インターフェース
public interface Animal {
void eat();
void sleep();
}
package zoo;
// アニマルインターフェースを継承したネコ科インターフェース
public interface Felidae extends Animal{
void run();
}
package zoo;
public class Caracal implements Felidae{
String name = "カラカル";
public void eat(){
System.out.println(this.name + "は、保護されているので、ご馳走をもらって食べている");
}
public void sleep (){
System.out.println(this.name + "は、素敵な寝床で寝ている");
}
public void run(){
System.out.println(this.name + "は、すばしっこく走っている");
}
}
package zoo;
public class Lion implements Felidae{
String name = "ライオン";
public void eat(){
System.out.println(this.name + "は勢いよく、肉に噛り付いた!");
}
public void sleep (){
System.out.println(this.name + "は大きなイビキをかいて寝ている");
}
public void run(){
System.out.println(this.name + "は、狭いところでは、あまり走れない");
}
}
package zoo;
public class Main {
public static void main(String[] args) {
// Felidae型の変数 c に Caracal インスタンス を格納
Felidae c = new Caracal();
// Felidae型の変数 l に Lion インスタンス を格納
Felidae l = new Lion();
c.eat();
l.eat();
c.sleep();
l.sleep();
c.run();
l.run();
}
}
クラス、インターフェースが沢山あってわかりづらいけれど、
ここで、重要なのは Mainクラス。
この一行が、何を表しているかが大事。
中身は ライオン !!! でも、 ネコ科 として捉える !!!
これは、継承の時に学んだ 「is - a の原則」と同じです。
(A is a B = A はBの一種である)
ライオン is a ネコ科 !になる。
なので、ここでも勿論、 is - a の原則から外れているものは代入できない。
たとえば
Food f = new Lion();
Zoo z = new Tiger();
上の例は代入できない。
それでも、無理やり利用することは出来るのでは?
と思うかもしれないけど
抽象メソッドは全てオーバーライドしないと、エラーになってしまうので
is - a の原則を無視しているものは、中途半端なインターフェース、クラスになってしまう。
(作りたい抽象メソッドがあっても、子クラスで必要ないものは書くことができなくなる。)
しかし、この is - a の関係、人間ならば、感覚で理解出来るけれど
コンピュータはどうやって判断しているのか?
実は「extends」「implements」で判断している。
この二つは、継承関係を Java に教えるためのものでもあったのだ。
上のコードだと、通常の
Lion l = new Lion();
Felidae l = new Lion();
の違いがわからない。
少しだけコードを変えてみる。
package zoo;
public class Lion implements Felidae{
String name = "ライオン";
// オーバーライド
public void eat(){
System.out.println(this.name + "は勢いよく、肉に噛り付いた!");
}
public void sleep (){
System.out.println(this.name + "は大きなイビキをかいて寝ている");
}
public void run(){
System.out.println(this.name + "は、狭いところでは、あまり走れない");
}
// 新規追加メソッド
public void fight(){
System.out.println(this.name + "は、自由を求めて戦った");
}
}
package zoo;
public class Main {
public static void main(String[] args) {
// Felidae型の変数 c に Caracal インスタンス を格納
Felidae c = new Caracal();
// Felidae型の変数 l に Lion インスタンス を格納
Felidae l = new Lion();
l.eat();
l.sleep();
l.run();
l.fight();
}
}
Lionクラスに
新しいメソッド fight を追加して
Mainクラスで fight を実行させようとしています。
しかし、これはエラーになります。
エラーメッセージをよくみると
メソッドfight()は型Felidae で未定義です
と書いてます。
コンピュータからみると、中身はLionでも、完全にFelidaeに見えているようです。
・呼び出し側は、同一視
・呼び出される側は、自分に決められた動きをする
ちょっとした実験。
package zoo;
public class Lion implements Felidae{
String name = "ライオン";
// オーバーライド
public void eat(){
System.out.println(this.name + "は勢いよく、肉に噛り付いた!");
}
public void sleep (){
System.out.println(this.name + "は大きなイビキをかいて寝ている");
}
public void run(){
System.out.println(this.name + "は、狭いところでは、あまり走れない");
}
// 新規追加メソッド
public void fight(){
System.out.println(this.name + "は、自由を求めて戦った");
}
}
package zoo;
public class Tiger extends Lion{
String name = "トラ";
// オーバーライド
public void eat(){
System.out.println(this.name + "は勢いよく、肉に噛り付いた!");
}
public void sleep (){
System.out.println(this.name + "は大きなイビキをかいて寝ている");
}
public void run(){
System.out.println(this.name + "は、狭いところでは、あまり走れない");
}
// 新規追加メソッド
public void fight(){
System.out.println(this.name + "は、自由を求めて戦った");
}
}
package zoo;
public class Main {
public static void main(String[] args) {
// Felidae型の変数 l に Tiger インスタンス を格納
Felidae t = new Tiger();
// Lion型の変数 t に Tiger インスタンス を格納
Lion l = new Tiger();
t.fight();
l.fight();
}
}
Lionクラスの下にTigerクラスを作ってみた。
(こういう is - a じゃないものが、やってはダメな例です。すみません!)
他にもちょこちょこっと変更があったけれど、ここで重要な Lion,Tiger,Mainのクラスのコードだけ載せます。
特に注目したいのがMainクラスの
// Felidae型の変数 l に Tiger インスタンス を格納
Felidae t = new Tiger();
// Lion型の変数 t に Tiger インスタンス を格納
Lion l = new Tiger();
この部分。
t.fight();
l.fight();
この実行結果は
どんな型に代入しようが、中身がどちらも Tiger だと、実行結果も Tiger になるということ。
一つ上の、エラーコードの例とこの例から言えることは
型の箱にないものは呼び出せないし(エラーになって、実行すらできない)、他がどうあれ中身の箱に入っているメソッドが呼び出され実行される。
ここで、一つ疑問。
Felidae型(fightメソッドをもっていない)の箱に入ってしまった Lion が Fight()メソッドを呼び出すにはどうしたらよいのか。
解決方法は「変数 l の中身を、 Lion であると捉えなおす」という方法。
そのために利用するのが「キャスト演算子」である。
package zoo;
public class Main {
public static void main(String[] args) {
// Felidae型の変数 l に Lion インスタンス を格納
Felidae f = new Lion();
f.eat();
f.sleep();
f.run();
// キャスト演算子によって、強制的に f を Lion型の変数 l に Lion インスタンスを格納
Lion l = (Lion) f;
l.fight();
}
}
このように、あいまいな型に入っている中身を、厳密な型に代入するキャストを
ダウンキャスト という。
本来、キャストはよほどの理由がない限る避けるもの。
なぜならば、失敗の危険が伴うから。
誤った代入を行った際は
「ClassCastException」
というエラーが出る。
これは、キャストにより強制代入の結果、「嘘の構図」になったため、強制停止せざるを得なくなった、という意味のエラーメッセージ。
誤った代入を未然に防ぐための演算子がある。
この instanceof演算子は
if文と組み合わせて使ったりできる。
package zoo;
public class Main {
public static void main(String[] args) {
// Felidae型の変数 l に Lion インスタンス を格納
Felidae f = new Lion();
f.eat();
f.sleep();
f.run();
// もし、 f の中身が Lionならば
if (f instanceof Lion){
// Lion型の変数 l に Lion インスタンスを格納
Lion l = (Lion) f;
l.fight();
}
}
}
これなら、もし中身が違う場合はダウンキャストが行われないので、エラーを防げる。
■ポリモーフィズムの利点
■
ここまで学んできたけれど、今のところ、利点といえるものは見当たらない。
それどころか、親クラスにはなくて、子クラスにしかないメソッドが使えなくなり、不便に感じる。
しかし、上手に利用するとポリモーフィズムの利点がわかってくる。
次のコードをみてみる。
package zoo;
// 継承の材料用のアニマル(動物)インターフェース
public interface Animal {
void eat();
}
package zoo;
// アニマルインターフェースを継承したネコ科抽象クラス
public abstract class Felidae implements Animal{
private int hungryPoint;
public int getHungryPoint(){
return this.hungryPoint;
}
public void setHungryPoint(int hungryPoint){
this.hungryPoint = hungryPoint;
}
public void recover(int i) {
}
}
package zoo;
public class Lion extends Felidae{
String name = "ライオン";
// オーバーライド
public void eat(){
System.out.println(this.name + "は勢いよく、肉に噛り付いた!");
}
@Override
public void recover(int i) {
System.out.println(this.name+ "は空腹度が" + i + "に回復した");
}
}
package zoo;
public class Caracal extends Felidae{
String name = "カラカル";
public void eat(){
System.out.println(this.name + "は、保護されているので、ご馳走をもらって食べている");
}
@Override
public void recover(int i) {
System.out.println(this.name+ "は空腹度が" + i + "に回復した");
}
}
package zoo;
public class Tiger extends Felidae{
String name = "トラ";
// オーバーライド
public void eat(){
System.out.println(this.name + "は勢いよく、肉に噛り付いた!");
}
@Override
public void recover(int i) {
System.out.println(this.name+ "は空腹度が" + i + "に回復した");
}
}
package zoo;
public class Main {
public static void main(String[] args) {
Lion l1 = new Lion();
Caracal c1 = new Caracal();
Tiger t1 = new Tiger();
l1.setHungryPoint(10);
c1.setHungryPoint(10);
t1.setHungryPoint(10);
l1.eat();
c1.eat();
t1.eat();
l1.recover(l1.getHungryPoint());
c1.recover(c1.getHungryPoint());
t1.recover(t1.getHungryPoint());
}
}
Mainメソッドに、重複したコードが多いように感じる。
引数が変更になった際の修正も面倒。
こういう一括で引数をわたしたいときなどは、配列と組み合わせるとスッキリする。
package zoo;
public class Main {
public static void main(String[] args) {
// Felidae型の要素を三つを 配列変数名 f に代入
Felidae [ ] f = new Felidae [3];
// 要素の中に Lion,Caracal,Tiger を入れる
f[0] = new Lion();
f[1] = new Caracal();
f[2] = new Tiger();
// 拡張for文を使いループをまわす
// f2 は Felidae型の任意の変数
for (Felidae f2 : f){
f2.eat();
}
for (Felidae f2 : f){
f2.recover(10);
}
}
}
実行結果はさきほどと変わらない。
こういう使い方をするのが、どういう場面かは、わからないけれど
上の例を見ると、とても綺麗なコードになったと思う。
これで、回復度を10→7に変える際も、わずかな労力で変更可能。
また、同じ animalクラスから受け継がれてきた eat()メソッドを一括で呼び出しても
中身(Lion,Caracal,Tiger)に合った動作をそれぞれが実行してくれる。
これが、ポリモーフィズムのすごいところ!