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(); に明示的に引数を渡さなければならない。

0 件のコメント:

コメントを投稿