Stylez Advent Calendar 2016の4日目です。
2016年春、まだJava初心者の私が始めて継承について知った...その時まとめていた記事です。
継承の基礎
###1. 似かよったクラスの開発をするときコピペをしてしまうことの問題点
大きなプログラムを書いていると、以前作ったクラスと似かよったクラスを作る必要が出てくることがある。
そういったときに元となるコードをコピー&ペーストして、新しい機能を追加してしまうと以下のようなデメリットが発生する。
- 追加・修正に手間がかかる
- コピー元のクラスに新しいメソッドを加えたときや、メソッドを変更した場合にコピーした後のクラスにも追加や変更を行なう必要がある。
- 把握や管理が難しくなる
- コピー元のクラスとコピーした後のクラスではソースコードの重複部分が多くなる。これによってプログラム全体の見通しが悪くなり、メンテナンスがしづらくなる。
###2. 継承による解決
似かよったクラスをコピー&ペーストで作成すると発生するデメリットを回避しつつ、類似したクラスの作成を可能にするのが継承という機能。
//継承したいクラス
Public class Hero {
private String name = "勇者";
Private int hp = 100;
//逃げる
public void run(){
System.out.println(name + "は逃げ出した!");
}
}
//Heroクラスを継承してもっと強い勇者にしたい・・・
Public Class SuperHero extends Hero { //ポイント1
public void attack(Matango m){
System.out.println(name + "の攻撃!");
m.hp -= 5;
System.out.println("5ポイントのダメージをあたえた!");
}
}
- 上記のコード内のポイント1の行にあるextendsに注目。SuperHero extends Heroという書き方をすることで、HeroクラスをベースにしてSuperHeroクラスを定義するので、Heroと同じメンバの定義は省略するという意味になる。
Class クラス名 extends 元となるクラス名 {
親クラスとの"差分"メンバ
}
- 継承を用いたクラスを定義するときは親クラス(継承の元になるクラスのこと)に追加したいメンバなど、差分のメンバを子クラス(新しく定義するクラスのこと)に書く。
- SuperHeroクラスがインスタンス化されるときに、JVMは「SuperHeroクラスはHeroクラスのrun()も持っている」と判断してくれる。
よって、Heroクラスを継承したSuperHeroクラスでは、SuperHeroクラスで定義したattack()メソッドだけでなく、Heroクラスのrun()メソッドも呼び出すことができる。
###3. 継承関係の表現方法
例ではHeroクラスを継承してSuperHeroクラスを作った。このような2つのクラスの関係を継承関係と呼び、継承関係は以下のように表せる。
Hero(親クラス、スーパークラス)
↑
SuperHero(子クラス、サブクラス)
※クラス図では、継承の矢印は子クラスから親クラスに向かって引くことを覚えておく。
###4. 継承のバリエーション
継承は1つの親クラスをベースにしてたくさんの子クラスを定義することもできる。
が、複数の親クラスを継承した1つの子クラスを定義すること(多重継承)はできない。
###5. オーバーライド
親クラスで定義しているメンバを子クラスで定義しなおしたい場面が出てきてしまった時、親クラスのメンバを子クラスで上書きすることができる。それをオーバーライドという。
◎継承を用いて子クラスに宣言されたメンバ
①親クラスに同じメンバがなければ、そのメンバは「追加」されたメンバになる。
②親クラスに同じメンバがあれば、そのメンバは「上書き変更」するメンバになる。
###6. 継承やオーバーライドの禁止
- 宣言時にfinalがつけられているクラスは継承できない。
public final class String extends Object....
- 宣言時にfinalがつけられているメソッドは、子クラスでオーバーライドができない
public final void slip()....
#インスタンスの姿
継承の基礎で作成したSuperHeroクラス・・・。このインスタンスは内部にHeroインスタンスを含んでおり、全体として二重構造になっている。この構造を意識して、さまざまな呼び出しや動作原理を理解していこう。
###1. メソッドの呼び出し
-
インスタンスの外部からメソッドの呼び出しがかかったとき、多重構造のインスタンスは極力外側にある子インスタンス部分のメソッドで対応しようとする。
例) 親クラスHeroと子クラスSuperHeroどちらにもrun()というメソッドが合った場合、run()の呼び出しで対応するのはSuperHeroのrun()である。 -
上記の例の場合、外部からの呼び出しでHeroのrun()メソッドが動くことはない
###2. 親インスタンス部へのアクセス
- 親インスタンス部へアクセスをする必要があるとき、親インスタンス部を表す予約語であるsuperを利用することで、子インスタンス部から親インスタンス部へアクセスが可能になる。
◎親インスタンス部のフィールドを利用する
super.フィールド名
◎親インスタンス部のメソッドを呼び出す
super.メソッド名(引数)
- super.run()なら親インスタンス部のrun()メソッドを呼び出すが、run()と書いてしまうと「this.run()」と同じ意味になり、自分自身で定義しているrun()メソッドを呼び出してしまう。
※Aクラス(祖父母)、Bクラス(親)、Cクラス(自分)という3つのクラスが継承関係にあるとき、cクラスは親クラスであるBクラスへのアクセスは可能だが、祖父母にあたるAクラスへ直接アクセスすることはできない。
#継承とコンストラクタ
###1. 継承を利用したクラスの作られ方
Heroクラスを継承しているSuperHeroクラスのインスタンスを生成するとき、SuperHeroのコンストラクタが呼び出されるが、実はこのときに継承しているHeroのコンストラクタも呼び出されている。
→**Javaには「すべてのコンストラクタは、その先頭で内部インスタンス部のコンストラクタを呼び出さなければならない」**というルールがあるため、このような動作をする。
- 本来はコンストラクタの最初の行にsuper(引数);を書かなければいけない。
- プログラマがsuper(引数);を書き忘れた場合、コンパイラにより自動的にsuper();が追加される。
###2. 親インスタンス部が作れない状況
「1.継承を利用したクラスの作られ方」で説明したように、super();は書き忘れても自動的に追加してくれる。
しかし、自動生成されたsuper();は引数なしで親クラスのコンストラクタの呼び出しを行おうとしてしまう。
このとき、親インスタンスに存在するコンストラクタに引数なしで呼べるものがなければ、呼び出せるコンストラクタがなくエラーの原因となってしまう。
###3. 内部インスタンスのコンストラクタ引数を指定する。
引数なしで呼べるコンストラクタがなく、呼び出しに失敗してしまった。どうするべきか・・・
◎コンストラクタの呼び出し時に引数を指定しよう。
public Item (String name){
というコンストラクタを呼び出したければ、super("ただの棒");のように引数をしているすることで、正しくコンストラクタの呼び出しを行うことができる。
このとき、呼び出すコンストラクタの引数の型、数に注意すること。
#正しい継承、間違った継承
###1. is-aの原則
正しい継承とは**「is-aの原則」といわれるルールに則っている継承**のこと
◎is-aの関係
子クラスis-a親クラス(子クラスは親クラスの一種である)
A is-a Bとは「AはBの一種である」という意味
継承をするときにis-aの文章を作ってみて、不自然ならその継承は間違っていることになる。
◎継承の利用に関するルール
is-aの原則が成立しないならば、ラクができても継承を使ってはならない。
###2. 間違った継承をすべきでない理由
is-aの関係ではない継承を使ってはならない理由は以下の2つ。
- 将来、クラスを拡張していったときに現実世界との矛盾が生じるから。
- オブジェクト指向の3大機能の最後の1つ「多様性」を利用できなくなるから。
###3. 汎化・特化の関係
is-aの関係で結ばれるということは、子クラスになればなるほど「特殊で具体的なもの」に具体化(特化)し、親クラスになればなるほど「一般的で抽象的・曖昧なもの」に一般化(汎化)していくことになる。
- 特化すると詳細にフィールドやメソッドを定めることができ、メンバが増える。
- 汎かすると詳細なフィールドやメソッドを定めることがしにくくなる。
継承は「コードの重複記述を減らすための道具」でもあり、「ある2つのクラスに特化・汎化の関係があることを示す」ための道具でもある。