Java

Java 継承

本投稿について

 スッキリわかるJava入門第2版を参考にしています。

1. 継承の基礎

1.1 継承

 プログラム開発を行う上では、以前作成したクラスと同じようなクラスを作成することがしばしばある。この時、以前作成したクラスのコピペをするのは、非効率であるため、以前作成したクラスを継承することで効率的なコードを書くことができる。

Hero.java
public class Hero{
  private String name = "hoge";
  private int Hp = 100;

  public void attack(Monster m){
    m.hp -= 5;
  }
  public void recover(){
    this.Hp += 5;
  }
}
SuperHero.java
public class SuperHero{
  private String name = "hoge";
  private int Hp = 100;
  private boolean flying;

  public void attack(Monster m){
    m.hp -= 5;
  }
  public void recover(){
    this.Hp += 20;
  }
  public void fly(){
    this.flying = true;
    System.out.println("飛行状態");
  }
  public void land(){
    this.flying = false;
    System.out.println("着地状態");
  }
}

 このコードでは、Heroクラスに新しいメソッドを追加した時に、その変更をSuperHeroクラスにも変更する必要がある。SuperHeroクラスとHeroクラスは、コードの大半が似通っているため、全体の見通しがしずらく、メンテナンスが面倒になる。そこで、継承を用いて、コードの重複をなくす。

SuperHero.java
public class SuperHero extends Hero{
  private boolean flying;

  public void attack(Monster m){
    m.hp -= 10;
  }  
  public void recover(){
    this.Hp += 20;
  }
  public void fly(){
    this.flying = true;
    System.out.println("飛行状態");
  }
  public void land(){
    this.flying = false;
    System.out.println("着地状態");
  }
}

以上のように、書き直すことで、Heroクラスのフィールド・メソッドを継承したSuperHeroクラスを定義することができる。この場合、Heroクラスが親クラス(スーパークラス)、SuperHeroクラスが子クラス(サブクラス)と呼ぶ。

1.2 継承のバリエーション

 Javaでは、多重継承を許可していない。具体的には、HeroクラスとMosterクラスがあった場合、それら2つのクラスを継承して、HeroMonsterクラスなるものを定義することはできない。つまり、複数の親クラスを親として、1つの子クラスを定義することはできない。
 親クラスに定義されているメソッドを子クラスで上書き定義することは可能である(メソッドのオーバーライドという)。Javaのクラスの中には、継承することを許されていないクラスも存在する。例として、Stringクラスが挙げられる。自分自身でも継承することを許可しないクラスの宣言をすることも可能である。クラスの宣言時に、final演算子を付け加える。

Main.java
public final class Main{
  public static void main(Strign args[]){
    //main関数の内容
  }
}

同様に、メソッドのオーバーライドを防ぐためには、メソッドの宣言時にfinal演算子を加える。

1.3 インスタンスの振る舞い

 継承によって、生成されたインスタンスの振る舞いがどうなるかについて考える。上記に示したHeroクラスと、SuperHeroクラスを用いて説明する。SuperHeroインスタンスは内部にHeroインスタンスを含んでおり、二重構造になっている。インスタンスの外部からメソッドの実行命令が呼びだされると、多重構造のインスタンスは、極力、外側にある子インスタンスのメソッドを呼び出す。具体的には、SuperHeroインスタンスから、recoverメソッドを呼び出すとオーバーライドされたメソッドが呼び出され、Heroインスタンスのrecoverメソッドは呼び出されない。
 親インスタンスへのアクセスについて説明する。追加仕様として、飛行状態で回復を行った場合は、Hpを25ポイント回復するとする。親インスタンスのメソッド・フィールドを呼び出す時は、

super.メソッド名(引数)
super.フィールド名

とする。

SuperHero.java
public class SuperHero extends Hero{
  private boolean flying;

  public void attack(Monster m){
    m.hp -= 10;
  }  
  public void recover(){
    this.Hp += 20;
    if(this.flying){
      super.recover();
    }
  }
  public void fly(){
    this.flying = true;
    System.out.println("飛行状態");
  }
  public void land(){
    this.flying = false;
    System.out.println("着地状態");
  }
}

この時、super.recover()を、recover()で呼び出すと、this.recover()として呼び出され、無限ループに陥ってしまうので注意。

1.4 継承とコンストラクタ

 継承されたインスタンスは、多重構造になっていることをこれまでに説明した。次に、インスタンスがどのように構築されていくかについて考える。上記のプログラムを参考にして、以下のプログラムを実行する。

Hero.java
public class Hero{
  private String name = "hoge";
  private int Hp = 100;

  public void attack(Monster m){
    m.hp -= 5;
  }
  public void recover(){
    this.Hp += 5;
  }

  Hero(){
    System.out.println("Heroコンストラクタ");
  }
}
SuperHero.java
public class SuperHero extends Hero{
  private boolean flying;

  public void attack(Monster m){
    m.hp -= 10;
  }  
  public void recover(){
    this.Hp += 20;
  }
  public void fly(){
    this.flying = true;
    System.out.println("飛行状態");
  }
  public void land(){
    this.flying = false;
    System.out.println("着地状態");
  }

  SuperHero(){
    System.out.println("SuperHeroコンストラクタ");
  }
}
Main.java
public class Main{
  public static void main(Strign args[]){
    SuperHero sh = new SuperHero();
  }
}

まずはじめに、親インスタンス部が生成され、次に外側に子インスタンス部が作られる。最後に、JVMによって自動的にSuperHeroクラスのコンストラクタが呼び出される。実行結果は、次のようになる。

実行結果
 Heroコンストラクタ
 SuperHeroコンストラクタ

内側インスタンスであるHeroインスタンスのコンストラクタも呼び出されていることに気がつく。これは、Javaのルールとして、「すべてのコンストラクタは、その先頭で内部インスタンスのコンストラクタを呼び出さなければならない。」となっているからである。

SuperHero.java
public class SuperHero extends Hero{
  .
  .
  .

  SuperHero(){
    super();
    System.out.println("SuperHeroコンストラクタ");
  }
}

コンパイラによって自動的に、super()が挿入される。

1.5 親インスタンスが生成できない状況

 親インスタンス部で定義されていないコンストラクタが、子インスタンス部のコンストラクタによって呼び出され、エラーとなってしまうことがある。具体的に、Humanクラスと、Soldierクラスを用いて説明する。

Human.java
public class Human{
  private String name;
  private int Hp;
  private int Mp;

  Human(String name){
    this.name = name;
    this.Hp = 100;
    this.Mp = 100;
  }
  Human(String name, int hp, int mp){
    this.name = name;
    this.Hp = hp;
    this.Mp = mp;
  }
}
Human.java
public class Soldier{
  .
  .
  .
}
Main.java
public class Main{
  public static void main(Strign args[]){
    Soldier soldier = new Soldier();
  }
}

このコードでは、Soldierクラスにコンストラクタがないため、親クラスであるHumanクラスのコンストラクタが呼び出される。しかし、Humanクラスには引数なしのコンストラクタがないため、このコードではエラーとなってしまう。それを防ぐために、Soldierクラスのコンストラクタで、強制的に引数ありのコンストラクタを呼び出せば、解決する。

Human.java
public class Soldier{
  .
  .
  .
  Soldier(){
    super("hoge");
  }
}

1.6 正しい継承と、間違った継承

 正しい継承とは、「is-aの原則」に従った継承のことである。

子クラス is-a 親クラス(子クラスは、親クラスの一部)

上記に示したコードで説明すると、SuperHeroクラスはHeroクラスの一部である。HeroクラスはSuperHeroクラスの一部であるのは間違いである。
継承は便利な機能であるが、間違った継承をするとプログラム上で矛盾が生じたり、多様性を利用することができなくなる可能性がある。

2. 抽象クラスを用いた継承

2.1 抽象クラスの存在意義

 抽象クラスとは、抽象的なメソッド・フィールドを持つクラスのことである。抽象クラスを継承した具象クラスは、抽象クラスで定義されているメソッドをオーバーライドしなければならない。また、複数の抽象クラスを継承する場合は、抽象クラスをインタフェースとして定義することによって実現可能となる(詳細は2.3)。
 抽象クラス・抽象メソッドの存在意義は、将来、開発者が増えていった場合、効率よく安全なプログラムを作るための材料である。具体例を挙げると、抽象クラス無しに開発を進めていくと、あるメソッドの内容が現段階では決まっておらず、メソッドの内容を空にしておくことがあるかもしれない。別の開発者がこのメソッドを見た時に、内容を書き換える必要があるのかが分からない。しかし、抽象メソッドとして定義してあれば、上書きする必要があるため、内容の書き換えが必要であることがわかる。この事から、抽象メソッドを用いることで、安心して開発を行うことができる。

2.2 抽象クラス・メソッド

Character.java
public class Character{
  .
  .
  public abstract void atttack(Monster m);

}

attackメソッドは、Characterクラスを継承したクラスで具体化させる。

Character.java
public abstract class Character{
  .
  .
  public abstract void atttack(Monster m);

}

未完成なクラスのインスタンスの生成を禁止にするために、クラスを抽象クラスとして定義する。

2.3 インタフェース

 Javaでは、特に抽象度が高い抽象クラスをインタフェースとして特別に扱うことができる。インタフェースとして扱うための条件として、

  • すべてのメソッドが抽象メソッド
  • 基本的にフィールドを1つも持たない(フィールドを定義した場合は、定数扱いとなる)

がある。

Creature.java
public interface Creature{
  public abstract void run();  //public abstractは省略可能
}

インタフェースに、変数を定義すると、public static finalが補われ、定数として扱われる。Creatureクラスを実装する時は、以下のようにする。

Pig.java
public class Pig implements Creature{
  public void run(){
    System.out.println("Pigが逃げ出した");
  }
}

インタフェースを定義することによって、

  • インタフェースをimplementsする複数の子クラスに、共通のメソッド群を実装するように強制でき、
  • あるクラスがインタフェースを実装していれば、少なくともそのインタフェースが定めたメソッドを持つことが保障される

というメリットがある。