○継承とは
オブジェクト指向の3大機能をひとつで、過去に作ったクラスを流用し、新しいクラスを簡単に作れる機能のことです。
継承を用いて開発することで、既存のクラスから、新しいクラスに**「変数定義」や「メソッド」**などを引き継ぐことができます。
○継承を使う場合と使わない場合
例として「Heroクラス」と「SuperHeroクラス」を用いて解説していきます。
◯Heroクラス
・「名前(name)」と「体力(hp)」のフィールドを持っている。
・「戦う(attack)」と逃げる(run)」のメソッドを持っている。
◯SuperHeroクラス
・2つのフィールドの他に「飛行(flying)」というフィールドを持っている。
・2つのメソッドの他に「飛ぶ(fly)」と「land(着地する)」というメソッドを持っている。
public class Hero {
String name = "勇者";
int hp = 150;
// 戦う(Slimeはモンスターの一種)
public void attack(Slime s) {
System.out.println(this.name + "の攻撃");
s.hp -= 10;
System.out.println("スライム" + s.suffix + "に5ダメージを与えた");
}
// 逃げる
public void run() {
System.out.println(this.name + "は逃げ出した");
}
}
<継承を使わない場合>
public class SuperHero {
String name = "伝説の勇者";
int hp = 300;
// 追加するフィールド
boolean flying;
// 戦う(Slimeはモンスターの一種)
public void attack(Slime s) {
System.out.println(this.name + "の攻撃");
s.hp -= 10;
System.out.println("スライム" + s.suffix + "に5ダメージを与えた");
}
// 逃げる
public void run() {
System.out.println(this.name + "は逃げ出した");
}
// 飛ぶ
public void fly() {
this.flying = true;
System.out.println(this.name + "は上空に飛んだ");
}
// 着地する
public void land() {
this.flying = false;
System.out.println(this.name + "は着地した");
}
}
上記の方法は、コードとして問題はなく、正しく動作しますが以下の2つの問題が発生します。
◯追加・修正に手間がかかる
もしHeroクラスに変更がある場合は、その変更をSuperHeroクラスにも行う必要があるため、手間がかかる。
◯把握や管理が難しくなる
SuperHeroクラスはHeroクラスを元に開発しているため、重複している部分があり、プログラム全体の見通しが悪く、メンテナンスすづらくなってしまう。
<継承を使う場合>
public class SuperHero extends Hero {
// 追加したフィールド
boolean flying;
// 飛ぶ
public void fly() {
this.flying = true;
System.out.println(this.name + "は上空に飛んだ");
}
// 着地する
public void land() {
this.flying = false;
System.out.println(this.name + "は着地した");
}
}
1行目のextendsは、クラスを継承する際に用いる記述で、継承元(スーパークラス)の情報(メンバ)の定義を省略することができます。
SuperHeroクラスがインスタンス化されるときに、JVMは「省略されているけれでも、SuperHeroクラスはHeroクラスに含まれているname、hp、attack()、run()を持っている」と判断してくれます。
なので、SuperHeroクラスのソースコードにはrun()はありませんが、インスタンス化されればrun()を呼び出すことができます。
public class Main {
public static void main(String[] args) {
SuperHero sh = new SuperHero();
sh.run();
}
}
<実行結果>
伝説の勇者は逃げ出した
このように、extendsを用いることによって、元となるクラスの「差分」だけを記述して新たなクラスを宣言することができます。
また、今回のような「Heroクラス」と「SuperHeroクラス」の関係を継承関係といい、継承元(Hero)のことを「スーパークラス」などと呼び、継承先(SuperHero)のことを「サブクラス」などと呼びます。
継承のバリエーションとしては、上記のような関係だけにとどまれず、1つのクラスをベースとして、複数の子クラスを定義することもできますし、孫クラスや曾孫クラスを定義することもできます。
しかし、Javaでは許されていない継承が1つだけあります。それが複数のクラスを親として1つの子クラスを定義する多重継承です。
複数のクラスを親にしてしまうと、親同士で同じ名前でありながら異なる内容の処理があり、それを継承してしまうとどちらの処理を動かすべきか混乱を招いてしまうためです。
○オーバーライド
親クラスを継承して子クラスを宣言する際に、親クラスのメンバ(フィールド、メソッド)を子クラス側で上書きすることをオーバーライドといいます。
先ほど宣言した「Heroクラス」から継承した「SuperHeroクラス」のrunメソッドを上書きしたい場合は以下の通りとなります。
public class SuperHero extends Hero {
boolean flying;
// 飛ぶ
public void fly() {
this.flying = true;
System.out.println(this.name + "は上空に飛んだ");
}
// 着地する
public void land() {
this.flying = false;
System.out.println(this.name + "は着地した");
}
// 逃げる(オーバーライド)
public void run() {
System.out.println(this.name + "は撤退した!");
}
}
public class Main {
public static void main(String[] args) {
Hero h = new Hero();
h.run();
SuperHero sh = new SuperHero();
sh.run();
}
}
<実行結果>
ミナトは逃げ出した!
ミナトは撤退した!
また、クラス宣言にfinalに付けると、継承禁止となり、メソッド宣言にfinalを付けると、オーバーライド禁止となります。
◯継承を利用したクラスのコンストラクタ
javaでは、すべてのコンストラクタは、その銭湯で必ず内部インスタンス部分(親クラス)のコンストラクタを呼び出さなければならないというルールがあります。
同じクラスの別コンストラクタを呼び出すための「this()」に似た**「super()」**と記述で親クラスのコンストラクタを呼び出すことができます。
super(引数);
もしコンストラクタの1行目でsuper()を呼び出していない場合、コンパイラによって**「super();」という行が自動的に挿入**されます。なので、引数を指定しない限りは、省略しても構わないです。
○is-aの原則
正しい継承とは、「is-a関係」というルールに則っている継承のことです。
「子クラス is-a 親クラス (子クラスは、親クラスの一種である)」
もし、「(子クラス)は(親クラス)の一種である」という文章を作って不自然さを感じたら、継承の誤りを疑いましょう。
<is-aの関係ではない継承をしてはいけない理由>
①将来、クラスを拡張していった場合に現実世界との矛盾が生じるから
②オブジェクト指向の3大機能の1つ「多態性」を利用できなくなるから。
正しい継承がis-aの関係で結ばれるということは、子クラスになるほど「特殊で具体的なもの」に具体化**(特化)していき、親クラスになるほど「抽象的であいまいなもの」に一般化(汎化)していきます。この関係を汎化と特化の関係**といいます。
継承は、「ある2つのクラスに特化・汎化の関係があることを示す」ための道具でもあります。