2014年10月22日水曜日

- 継承 後編- Java

今回は「継承」の続き。
主に「抽象クラス」「インターフェース」について学ぶ。

内容は、そこまでは難しくない。
ただ、感覚が今までとは違う。

この、「抽象クラス」「インターフェース」は、「今」よりも「未来」に焦点をあてている。
今の自分のため!では、なく、第三者、未来の自分のために必要な道具である。

今はまだ、詳細が決まっていないメソッドがあるかもしれない。
そういうときに、役に立つのが、「継承」である。



抽象クラス

まず、継承の材料となるクラス(親クラス)を作る。
今までは、Catクラス、StrayCatクラス、BossStrayCatクラスなどを作ってきた。
Catクラスを使こともあったけれど、
今後は、「継承の材料となるクラス」は、newせずに使う(継承の材料のみの)クラスにしたい。

猫の話は、飽きてきただろうから、別のものを作る。

マリオブラザーズを作ることを考えてみます。(実際、ここでは作りません)
マリオがいて、敵が沢山いて。
敵が何をするかは、まだ考えていない。
けど、移動、攻撃、倒されるメソッドは必要だなー。

こういうとき、メソッドの中身を空にしておくという手もあるといえばある。
けれど、後で見返したときに、「実際に何もしない空のメソッド」なのか、「後から書き足す予定で空にしておいているメソッド」なのか、区別がつかない。
(コメントに書くという手もあるけれど、もし膨大な空のメソッドがあったら、大変ですし、他人や、未来の自分への負担が増える)

そこで、「抽象メソッド」というものを利用する。









この抽象メソッドの利用には、制約がある。

一つでも抽象メソッドを含むクラスは、「抽象クラス」というものにしなくてはいけない。
(抽象クラスになると、エラーが出る)












package blog;

public abstract  class Monster {

 // 以下、詳細未定メソッド
 public abstract void atack();

 public abstract void dying();

 public abstract void move ();
}


詳細未定メソッド atack,dying,move を定義した抽象クラス monster を書いた。

さらに、抽象クラスには制約がある。

抽象クラスは、newによるインスタンス化が禁止される!!
(newしようとすると、エラーが出る)

これが、上で書いた「抽象クラスはnewせずに使う、継承の材料のみのクラスにしたい」理由。
この制約により、何も知らない第三者が、継承の材料(と想定して作ったクラス)を誤ってnewすることもなくなる。

さらに、優れた機能がある。
まずは、この抽象クラスである monsterクラスを継承させた子クラスを作ってみる。
















上の画像は、 抽象クラスであるMonsterクラスを継承(extends)した Kuribouクラス を定義した際に現れたエラーメッセージです。

このエラーを消す方法は2つある

①Kuribouクラスの宣言の際に、 abstract をつける
②抽象メソッドを全てオーバーライドする

上に書いたようにabstract がついていたら、new出来ないため、
Kuribouクラスをインスタンス化させるためには、実質、全てオーバーライドする以外の方法はない。
なので、第三者、もしくは、未来の自分にオーバーライドを強制できる。



package blog;

public class Kuribou extends Monster {

 public void atack(){
  System.out.println("のんびり歩いて体をぶつけるよ!");
 }
 public void move(){
  System.out.println("左右にのんびり歩くよ!");
 }
 public void dying(){
  System.out.println("踏まれたらつぶれちゃうよ!");
 }
}
















オーバーライドによって、エラーメッセージは綺麗に消えました!


インターフェース

前回の継承で話したように、継承の階層が下がるにつれ、特化、あがるにつれ汎化する。
つまり、上流のクラスは、すべて抽象クラスになる。

特に抽象度が高いクラスは インターフェース にできる。
条件は

①すべてのメソッドが抽象メソッド
②基本的にフィールドを一つももたない。
(public static final がついたフィールドのみ許される。また、public static final は省略可能であり、フィールドを宣言すると自動的につく)











ここでも、ルールがあり、インターフェースとして宣言されたもののメソッドは、
自動的にpublic かつ abstract になる
(public abstract は省略可能。省略するのが一般的。)

さきほど、作った抽象クラス Monster は、全て抽象メソッド、フィールドも持たないという条件を満たしているので、インターフェースにしてみる。


package blog;

public interface Monster {

 // 以下、詳細未定メソッド
 void atack();

 void dying();

 void move ();
}


コードがスッキリしましたね。
しかし、ここで問題が出てきます。


























また、子クラスである Kuribouクラスにエラーメッセージが出てきました。
エラーメッセージに書いてあるように、
インターフェースを継承した子クラスを定義する際は、
extends ではなく implements を使う必要がある。
また、この行為は「実装する」と表現する。













package blog;

public class Kuribou implements Monster {

 public void atack(){
  System.out.println("のんびり歩いて体をぶつけるよ!");
 }
 public void move(){
  System.out.println("左右にのんびり歩くよ!");
 }
 public void dying(){
  System.out.println("踏まれたらつぶれちゃうよ!");
 }
}

extends の部分を implements に書き直して無事、エラーは消えた。


インターフェースって、そもそもなんだ??と思うけど、今は抽象クラスの親玉!!(あいまいなことしか言わない親玉)と思っておけば問題ない。
結局は、抽象クラスと継承の仕方は変わらず、Kuribouクラスでオーバーラードしなければnew出来ない。

つまり、
・このインターフェースを implements する複数の子クラスたちは、共通のメソッドを実装するように強制できる
・あるクラスがこのインターフェースを実装していれば、少なくとも、インターフェースが定めたメソッドは持っていることが保障される
(何度も言うが、オーバーライドしないと、new出来ないため!)

これだけみても、インターフェースと抽象クラスの違いがわからないと思う。
が、最大の強みがインターフェースには備わっている。

インターフェースにのみ許される多重継承

実は、Javaでは出来ないと思われていた(私が思っていただけですが!)多重継承が可能になる!

なぜ、多重継承がインターフェースにのみ許されているかというと……。
インターフェースをおさらいすると、抽象メソッドしかない。フィールドも持っていない。
つまり、内部実装がいっさい定められていない状態である。



















歩くモンスターインターフェース、飛ぶモンスターインターフェースを多重継承を利用して実装して
パタクリボー(空を飛ぶクリボーです)で、抽象メソッドである move() をオーバーライドする。
これは、なんの問題もない。

ダメな例(インターフェース以外から多重継承を試みる)は以下。


















歩くモンスタークラス、飛ぶモンスタークラス、どちらもメソッド move()があると、
実装の際に、どちらの動き方をするのかわからなくなってしまう。

そういう混乱を防ぐため、インターフェース以外からの多重継承は許されていない。
















package blog;

public  interface WalkMonster {

 void move();

}


package blog;

public  interface FlyingMonster {

 void move();

}


package blog;

public class PataKuribou implements FlyingMonster,WalkMonster {

 public void move (){
  System.out.println("飛んだり歩いたり!!");
 }

}


三つ以上のインターフェースを実装可能。

また、インターフェース同士の継承も可能。
その際は、implements ではなく、 extends と書く。
(クラス同士、インターフェース同士は extends)

クラスとインターフェースを両方使ったクラス定義も可能。













上手い例が思い浮かばなかったため、コードは書けない。
オーバーライドしつつ、インターフェースから継承した抽象メソッドを実装することができる。

2014年10月21日火曜日

- 継承 前編- Java

今日は、三大要素の二つ目「継承」について。


プログラムを書いていると似通ったクラスを作ることがある。

















この三つのクラスが必要になったとき、どうやって解決するか。

最も安易な解決方法は 「コピー&ペースト」


package blog2;

public class Cat {
 void eat (){
  System.out.println("食べる");
 }
 void sleep (){
  System.out.println("寝る");
 }
}

package blog2;

public class StrayCat {
 void eat (){
  System.out.println("食べる");
 }
 void sleep (){
  System.out.println("寝る");
 }
 void cadge(){
  System.out.println("ねだる");
 }
 void escape(){
  System.out.println("逃げる");
 }

}

package blog2;

public class BossStrayCat {

 void eat (){
  System.out.println("食べる");
 }
 void sleep (){
  System.out.println("寝る");
 }
 void cadge(){
  System.out.println("ねだる");
 }
 void escape(){
  System.out.println("逃げる");
 }
 void fight (){
  System.out.println("戦う");
 }

}

コピペを駆使して、作ってみた。
この程度なら、すぐに済む。
けれど、大きな問題が二つある。

①追加、修正が非常に大変
②把握や管理が難しくなる

コピペ戦法は、クラスもメソッドも多くない場合にしか、使えない。
実質、使えない。(大きな開発では、クラスがたくさんある)

最適な解決方法が 「継承」



















package blog2;

public class Cat {
 void eat (){
  System.out.println("食べる");
 }
 void sleep (){
  System.out.println("寝る");
 }
}


package blog2;

public class StrayCat extends Cat {
 void cadge(){
  System.out.println("ねだる");
 }
 void escape(){
  System.out.println("逃げる");
 }

}

package blog2;

public  class BossStrayCat extends StrayCat{

 void fight (){
  System.out.println("戦う");
 }

}


StrayCatクラス は Catクラス を元にして
BossStrayCatクラス は StrayCatクラス を元にした。
コードがスッキリしましたね。



















簡単なクラス図で表すとこうなる。
ボス猫が一番強そうだけど、あくまでも、親はCatクラス。

継承には、いくつか決まりがある。




























上のように、一つの親クラスから複数の子クラスへの継承は出来るが、
下のように一つの子クラスからの多重継承はできない。(Javaでは!)


継承そのものにも決まりがある。

is - a の原則

A is a B (AはBの一種である)
という、英文に基づいた継承でなければならない。

正しい継承
子クラス is a 親クラス (子クラスは親クラスの一種である)

間違った継承
is - a の関係がない
例 マラソンクラス サッカークラス


間違った継承がダメな理由

①将来、クラスを拡張していった場合に現実世界との矛盾が生じる
  (オブジェクト指向の原則から外れる)
②三大要素の一つである「多態性」を利用出来なくなる

便利かどうかは関係ない!!

マラソンクラスとサッカークラスでは、同じ「走る」動作があっても、走り方は全然違う。
マラソンは「走る」競技だけど、サッカーは、「点を取る」競技。

このルールを守って正しい継承を利用すると
子クラスになるほど「特殊で具体的なもの」に特化する
親クラスになるほど「一般的で抽象的、曖昧なもの」に汎化する
(クラス図の継承関係を表す矢印は「クラスが汎化していく方向」を表す)

継承の利用によって出来ること
・コードの重複記述を減らす
・2つのクラスに、特化、汎化関係があることを示す



さて、ここで問題。
Catのeat と StrayCatのeat
同じ、食べる行為でも、違う文を表示したい場合。
野良猫なら、お腹をすかせているから、貪るように食べるかもしれませんね。

インスタンスの多重構造

継承の際、内部で何が起きているのか。



























StrayCatクラスをインスタンス化すると、Catクラス部分を持ったStrayCatインスタンスが出来上がる。
このように、継承の際は、インスタンスが二重構造になる。
(親、子、孫の場合は三重構造)




オーバーライド

親クラスを継承して、子クラスを宣言する際に、子クラス側のメンバを優先させることができる
これを オーバーライド という。



package blog;
package blog2;

public class StrayCat extends Cat {

 // 親クラスに定義してあるメソッドをオーバーライド
 void eat (){
  System.out.println("ガツガツ餌を食べた!!");
 }
 // 新規追加したメソッド
 void cadge(){
  System.out.println("ねだる");
 }
 // 新規追加したメソッド
 void escape(){
  System.out.println("逃げる");
 }

}

※注意※
宣言にfinalがついているクラスは継承ができない
宣言にfinalがついているメソッドは子クラスでオーバーライドできない


では、オーバーライドの際に、内部がどうなっているのかをみてみる。



























Cat でも StrayCat でも eat ( ) を持っている。
多重構造のインスタンスは極力、外側にある、子インスタンス部分のメソッドで対応する。
(が、実際には、内側のインスタンス部分のものから順に呼ばれている。後で説明。)

StrayCatの仕様が変更され、
StrayCatが空腹の場合は、「ガツガツ食べ」、そうではない場合は、Catのeat( ) メソッドと同様に普通の食べ方をさせてい場合。
親インスタンス部分へアクセスする方法がある。


package blog2;

import java.util.Random;

public class Cat {

 // フィールド hungryPoint を追加。0~4のランダムな整数が代入される
 int hungryPoint = new Random().nextInt(5);

 public void eat (){
  System.out.println("食べる");
 }
 public void sleep (){
  System.out.println("寝る");
 }
}

package blog2;

public class StrayCat extends Cat {

 // eat() メソッド
 public void eat(){
  System.out.println("ガツガツ食べた");

 // 親クラスの hungryPoint が3以下の場合
 if (super.hungryPoint <= 3){
  // 親クラスの eat()を呼び出す
  super.eat();
  }
 }

 // 新規追加したメソッド
 void cadge(){
  System.out.println("ねだる");
 }
 // 新規追加したメソッド
 void escape(){
  System.out.println("逃げる");
 }

}





この時に、使う super とは、「親インスタンス部」を表す予約語であり、
子インスタンス部分から、親インスタンス部分のメソッドやフィールドにアクセスする際に用いる。

注意事項があり、これは、子から親までのアクセスしかできない。
子から、祖父母(三重構造)へのアクセスはできない。

継承とコンストラクタ

new StrayCat ( ) した場合に、どういう順序でコンストラクタが呼ばれるか。

①まず、親インスタンス部が作られる
②外側に、子インスタンス部が作られる
③JVM(Java仮想マシン)により、自動的にコンストラクタが呼ばれる

ここで、次のコードをみてみる。


package blog3;

public class Cat {

 public Cat(){
  System.out.println("Cat出撃!!!");
 }
}


package blog3;

public class StrayCat extends Cat {

 public StrayCat(){
  System.out.println("StrayCat出撃!!!");
 }
}

package blog3;

public class Main {

 public static void main(String[] args) {

  StrayCat sc = new StrayCat();

 }
}


このコードの実行結果は以下。








Mainクラスで、StrayCat のみを インスタンス化しただけなのに
親クラス Cat の コンストラクタも呼び出されている。

実は、Javaでは、全てのコンストラクタは、その先頭で必ず親クラスのコンストラクタを呼び出さなければならないというルールがある。

親クラスのコンストラクタを呼び出すには

super (引数);

という一行を加えればよい。


package blog3;

public class StrayCat extends Cat {

 public StrayCat(){
  super();
  System.out.println("StrayCat出撃!!!");
 }
}

なので、本来は、このようにコンストラクタの一行目に
super();
を書かなければならない。
が、書いていない場合は自動的に super(); という行が挿入される。

自動で追加される super(); は、引数がない(0個)ということに注意しなくてはいけない。
引数が0個のコンストラクタが存在しない場合には、エラーが発生する。

その場合には、super(); に明示的に引数を渡さなければならない。