継承ついて

 Javaで大きなプログラムを作り始めると、以前作ったクラスと似通ったクラスを作る必要に迫れられことがあります。 そういったときどうすればよいでしょうか。まず、Heroクラスを見てみます。

public class Hero{
String name = "ミナト";
int hp = 100;
public void attack(matango m){
したい内容を書く
}
public void run(){
したい内容を書く
}
}
 これが進化するにつれて、superHeroになるとします。superHeroはこれに加えて、空を飛ぶことができます。それをHeroクラスをもとに 作ったsuperHeroクラスを見てみます。
public class superHero extends Hero{
boolean flying;
public void fly(){
したい内容を書く
}
}
 Heroクラスにfly()をコピペすれば、もちろんsuperHeroクラスを作成することができます。しかし、変更の都度コピペを繰り返していては、 手間がかかり効率が悪いです。そこで、extends 元となるクラスを用いた継承という機能を用います。これを用いることで、元となるクラスの 差分だけを記述して新たなクラスを宣言することができるのです。今回の例では、Heroクラスを継承してsuperHeroクラスを作りました。このような2つのクラスの関係を 継承関係といいます。元となるクラスを「親クラス」「スーパークラス」と呼び、新たに定義されるクラスを「子クラス」「サブクラス」と呼びます。 ここで注意すべきことは、複数のクラスを親として1つのクラスを定義する多重継承はJavaでは許されていないということです。 コードを書き進めていくと、クラスの中の記述内容を書き換えたいときが出てきます。これをいちいち書き換えていては、やはり効率が悪いように感じます。 そこで、親クラスを継承して子クラスを宣言する際に、親クラスのメンバを子クラス側で上書きすることができます。これをオーバーライドといいます。 以下にその例を示しておきます。
public class superHero extends Hero{
boolean flying;
public void fly(){
したい内容を書く
}
//これが親クラスのメンバの上書きしたい内容
public void run(){
したい内容を書く
}
}
 上書きした内容を実際に呼び出してみます。
public class Main{
public static void main(String[] args){
Hero h = new Hero();
h.run;
superHero sh = new superHero();
sh.run();
}
}
実行結果
Heroクラスのrun()の内容
superHeroクラスで上書きされたrun()の内容
 このように親クラスのメンバの上書きを子クラスで行うことができます。また、親クラスのメンバに同じメンバがなければ、そのメンバは追加されることに 注意してください。ここですべてのクラスが継承できるのかという疑問を抱くと思います。結論から申し上げますとすべてのクラスを継承することはできません。 では、どのようなクラスが継承できないのでしょうか。これは、宣言時にfinalがつけられているクラスは継承できないというルールがあります。finalトいうことですから、 それは定数を表しています。つまり、定数として定義されたクラスは継承禁止ということなのです。また、宣言にfinalがつけられたメソッドは、子クラスでオーバーライドすることができない のも特徴としてあります。逆に言うと継承させたくないクラスやオーバーライドされたくないメソッドがあれば、宣言時にfinalをつければいいのです。 ここで継承を使いこなすには、より踏み込んで継承について考える必要があります。継承されたインスタンスのイメージは、superHeroの胸の中に普通のHeroとしての自分を秘めているとい 感じです。そのため、インスタンスの外部からメソッドの実行依頼が届くと、極力、外側にある子インスタンス部分のメソッドで対応しようとします。 ここで以下のようなコードを考えます。
public class superHero extends Hero{
public void attack(Matango m){
System.out.println(this.name+"");
m.hp -= 5;
System.out.println("");
if (this.flying){
System.out.println("");
m.hp -= 5; System.out.println("");
}
}
}
 このコードでは空を飛んでいる状態でattack()すると、Heroでは1回だった攻撃を2回連続で繰り返すことができます。 しかし、これではHeroクラスのattack()内容が変更されたときに困ります。仮に与えるダメージを10にしたとしましょう。そうすると、本来なら 10ダメージを連続で食らわせることができますが、子クラスであるsuperHeroクラスで親クラスのattack()がオーバーライドされてしまうので、ダメージは5のままになってしまいます。 そこで、ダイレクトに親クラスのattack()を呼び出せたら、この問題を解決することができるでしょう。これを実現するのがsuperという予約語です。 これは「今より1つ内側のインスタンス部分」を表します。こうすることで、親インスタンス部分のメソッドやフィールドに子インスタンス部分からアクセスることができるようになりました。
super.フィールド名
super.メソッド名(引数)
ここまでHeroとsuperHeroを例に継承について考えてきました。インスタンス化されたsuperHeroは中にHeroのインスタンスをもつ多重構造になっていることが ここまででわかりました。ここまで出来たら、JVMはsuperHero()コンストラクタを自動的に呼び出します。そうすると、Hero()コンストラクタも勝手に動作してしまうのです。 実は、Javaでは全てのコンストラクタは、その先頭で必ず内部インスタンス部分(親クラス)のコンストラクタを呼び出さなければいけないというルールがあります。 つまり、コンストラクタは内側のインスタンス部分のものから順に動いていくのです。このようにインスタンスが構築・初期化される手順を理解すると、ある条件で困ったことが発生します。 まずは以下のコードを見てください。

Item.javaファイル

public class Item{
String name;
int price;
public Item(String name){
//処理内容
}
public Item(String name, int price){
//処理内容
}
}

Weapon.javaファイル

public class Weapon extends Item{//処理内容}

main.javaファイル

public class Main{
public static void main(String[] args){
Weapon w = new Weapon();
}
}
 このコードを実行するとエラー出てしまいます。その原因を探ってみましょう。Weapon.javaファイルに注目します。
public Weapon(){
}
 Main.javaファイルで二重構造のインスタンスを作り終えると、JVMは自動的にWeapon()コンストラクタを呼び出そうとします。しかし、Weaponクラスには コンストラクタが定義されていないため、暗黙の了解で「デフォルトコンストラクタ」が定義され動作します。しかし全てのコンストラクタには先頭行に実は super()が隠れていますので、親クラスのItemのコンストラクタを引数なしで予防とします。ここに問題点があります。呼び出されたItemクラスを見てみると、 引数1つのものと引数2つのもの、あわせて2つのコンストラクトが宣言されていますが、引数0のコンストラクトは存在しません。そのために、エラーが出るということになります。 そこで、super("引数")で親クラスのコンストラクトを呼んであげれば、エラーが出ずに実行できるというとになるのです。 ここまでで継承についての説明はほとんどすべて終わりました。最後に正しい継承、間違った継承について説明していきます。正しい継承とは「is-aの原則」というルールに従っている 継承のことです。これは、子クラスは親クラスの一種であると考え方です。間違った継承とはこの関係を満たしていない継承のことを言います。 では、間違った継承は何がいけないのでしょうか。理由は2点あります。・将来、クラスを拡張していった場合に現実世界との矛盾が生じるから、・オブジェクト指向の3大機能の1つ多様性を利用できなくなるからです。 現実世界との矛盾が生じないように継承することが大事なのです。

高度な継承ついて

 私たちは現実世界において、無意識に多くのものを抽象的に捉え、利用しています。オブジェクト指向の目的が「現実世界の再現」である以上、Javaでも 「抽象的な登場人物」を上手に捉える必要があります。ここでは、抽象的であいまいなクラスを正しく・安全に・便利に利用するために準備されているクラスの 定義方法について説明していきます。高度な継承と小難しい言い回しになっていますが、簡単に言うと他の開発者が効率よく安心して利用できる継承の材料を作ることです。 しかし、ここで2つの不都合、3つの心配事が生じます。まず以下のコードを見てください。

public class Character{
String name;
int hp;
public void run(){
System.out.println(this.name+"は逃げ出した");
}
public void attack(Matango m){
System.out.println(this.name+"の攻撃!");
m.hp -= ??;
}
}
 ここで生じる不都合はattack()メソッドの内容を確定できないという点です。なぜなら、敵(Matango m)に与えるダメージは攻撃する キャラクターによって違うからです。ではattack()メソッドを書かなければいいじゃないかと思いつくかもしれませ。しかし、これではキャラクターであれば 少なくとも攻撃できるはずというゲーム世界の前提とずれてしまうことになります。そのため、これはいい解決策とは言えません。 これに対する解決策として、attack()メソッドの内容を空にしておいて、他の開発者が継承して、それぞれのキャラクターのクラスを作成するときに、その職業に沿った 最適なattack()メソッドでオーバーライドしてもらうという考え方が有用です。ここでまた、心配事が2点生じます。1点目はオーバーライド忘れです。これではmain()メソッドなどからattack()メソッドは呼び出せますが、 何も起きないという不具合を抱えたクラスになってしまします。コメントを残しておくというのは解決策の1つとして挙げられます。 2点目は「本当に何もしない」と区別がつかないです。本来メソッドの中が空ということは何もしないとみなされがちですが、この場合は何をするかが未定で記述できていないので、区別がつかない恐れがあるということです。 ここまではクラスに関する2つの心配事について考えていました。実は全く別の観点からの心配事があります。それは未来の開発者が間違ってCharacterクラスをnewして利用してしまうということです。 そもそもCharacterクラスは継承の材料として使われるものであって、newして使うものではないということが、この心配事の根底にあります。 これはクラスには2つの利用方法(newによる利用、継承による利用)があり、開発者はそれを自由に選ぶことができるという不都合でもあるのです。 これらの心配事を解決する仕組みがJavaには存在します。その解決方法について1つ1つ見ていきましょう。 まずは、第2の心配事についてです。これは何だったかというと「空のメソッドを作っておくと、現時点で処理内容を確定できないメソッドなのか何もしないメソッドなのか、区別がつかない」というものでした。 実はJavaには、「詳細の未定のメソッド」を記述する専用の構文が用意されています。
public abstract 戻り値の型 メソッド名(引数 リスト);
 これを抽象メソッドといいます。重要なのは詳細未定のメソッドの前にabstractとつけるということです。 次に、第3の心配事についてです。これは、「未完成部分(抽象メソッド)を含む継承専用のクラスを誤ってnewされる可能性がある」というものでした。 Javaでは、抽象メソッドを1つでも含むクラスは、以下の構文で書かなければいけません。
public abstract class クラス名{

}
 このように、abstractがついたクラスを抽象クラスと呼びます。これをすることで、newによるインスタンス化を禁止することができます。 これを忘れてしまうと、コンパイルエラーが出てしまうので注意が必要です。 最後は、第1の心配事についてです。これは、「未来の開発者が、詳細未定のメソッドをオーバーライドし忘れる可能性がある」というものでした。 この心配事は今までのことを踏まえて解決することができます。例えば、抽象クラスを継承して、新たなクラスを作成したとしましょう。抽象メソッドをオーバーライドせずにコンパイルすると コンパイルエラーが出てしまいます。実は、新たなクラスは、親クラスの抽象クラスから継承した抽象メソッドを持っているのです。そのため、新たなクラスも抽象クラスにする必要があります。 しかし、これをしてしまうとnewをして、インスタンス化することができなくなってしまいます。そこで、未完成の部分の抽象メソッドをオーバーライドしてあげることによって、 問題を解決することができます。こうして、抽象クラスを継承した新たなクラスはnewして使えるクラスになりました。また、オーバーライドをしなければコンパイルエラーが出てしまうことから、 オーバーライドを強制することができるようになりました。以上で、3つの心配事を全て解決しました。抽象クラスと抽象メソッドを用いることで、未来の開発者が安全かつ便利に利用できる「継承の材料」 となるクラスを開発できるようになったのです。

インタフェース

 ここまで、継承階層を下にたどっていくとどうなるかを見ていきました。下がっていくたびに具体化していき、最終的に全てのメソッド につて処理内容が実装されていくのでした。では、今度は逆に、階層を下から上に昇ってみることを考えましょう。すると、上にいくにつれて「どんどんあいまいなものになっていく」、そして、「どのような内部情報を持っているか(フィールド)」 「どのような動きをするか(メソッド)」は、あやふやになり、決めることができなくなっていくのです。Javaでは、特に抽象度が高い抽象クラスを、インタフェースとして特別に扱うことができます。 インタフェースとして特別扱いできる条件は、「全てのメソッドは抽象メソッドである」「基本的にフィールドを1つも持たない」です。この時、以下のような構文を用いてインタフェースとして定義します。

public interface インタフェース名{

}
 このようにインタフェースに宣言されたメソッドは、自動的にpublicかつabstractになるというルールがあります。なので、abstractを省略しても問題ありません。 また、定数のみ宣言が許されています。それでは例を見てみましょう。

CleaningServiceインタフェース

public interface CleaningService{
Shirt washShirt(Shirt s);
Towl washTowl(Towl t);
Coat washCoat(Coat c);
}

インタフェースを実装したクラスの定義

public class KyotoCleanigShop implements CleaningService{
String ownerName;
String address;
String phone;
public Shirt washShirt(Shirt s){
return s;
}
public Towl washTowl(Towl t){
return t;
}
public Coat washCoat(Coat c){
return c;
}
}
 このように、インタフェースを継承して子クラスを定義する場合はextendsではなく、implementsを使います。 インタフェースの効果として、「同じインタフェースをimplementsする複数の子クラスたちに、共通のメソッド群を実装するよう強制できる」「あるクラスがインタフェースを実装していれば、少なくともそのインタフェースが定めたメソッドは持っていることが保証される」 といったところです。インタフェースは、内部実装を一切定めていません。これが、特別扱いされる理由ですが、この性質のおかげでインタフェースは特別に多重継承が許されています。 そもそも多重継承が問題なのは、「両方の親クラスから同じ名前でありながら異なる内容のメソッドを継承して衝突してしまう」空です。しかし、両方の親がインタフェースの場合、 どちらもメソッドの内容を定めていませんので、「親から継承した2つの処理内容が衝突する」ことは起こりえないのです。そのために、複数の親インタフェースによる多重継承が許されています。

インタフェースによる多重継承

public class クラス名 implements 親インタフェース名1, 親インタフェース2, ////{

}
 ところで、あるインタフェースを定義する場合、ゼロから開発せずに既存のインタフェースを継承してその機能を拡張することもできます。 ここで、extends(拡張)とimplements(実装)の使い分けは混乱しやすいですが、同種(クラス同士、インタフェース同士)の継承の場合extends異種ならimplementsと覚えておけば 問題ありません。また、クラス定義の際にextendsとimplementsの両方を利用することもあります。

extendsとimplementsの両方を使ったクラス定義

public class クラス名 extends 親クラス implements 親インタフェース1, 親インタフェース2, ///{

}