Programming 3 / Java

トップに戻る

クラスの継承・super・this

既存クラスを拡張する継承の仕組みと、親子のコンストラクタがどの順序で呼び出されるかを学び、IS-AとHAS-Aを使い分けます。

既存のクラスを拡張して新しいクラスを作ることを継承といいます。Javaでは extends を使います。

継承の基本形
class NamedRectangle extends Rectangle {
  // NamedRectangle独自の定義
}
Rectangleスーパークラス・親クラス
↓ extends
NamedRectangleサブクラス・子クラス
Javaのクラスが直接継承できるクラスは1つだけです。ただし、継承を何段階にもつなげることはできます。

extends を書いていないクラスも、最上位の java.lang.Object を継承しています。

暗黙の継承
class Rectangle {
}

// おおむね次と同じ意味
class Rectangle extends Object {
}
Rectangle.java / NamedRectangle.java
class Rectangle {
  int width;
  int height;

  void setSize(int width, int height) {
    this.width = width;
    this.height = height;
  }

  int getArea() {
    return width * height;
  }
}

class NamedRectangle extends Rectangle {
  String name;
}

NamedRectangle に直接書かれていない widthheightsetSize()getArea() も、継承によって利用できます。

フィールドやメソッドとは異なり、コンストラクタはサブクラスへ継承されません。

Rectangleのコンストラクタ
Rectangle(int width, int height) {
  this.width = width;
  this.height = height;
}
Rectangle(int, int) があっても、対応するコンストラクタを子クラスに定義しない限り、new NamedRectangle(123, 45) とは書けません。

super()で親のコンストラクタを呼ぶ

NamedRectangleのコンストラクタ
NamedRectangle(String name) {
  super(200, 32);
  this.name = name;
}

this() を使うと、同じクラスに定義された別のコンストラクタを呼び出せます。これも必ず先頭に書きます。

NamedRectangle.java
NamedRectangle() {
  this("NO NAME");
}

NamedRectangle(String name) {
  super(200, 32);
  this.name = name;
}
new NamedRectangle()
NamedRectangle()
this("NO NAME")
super(200, 32)
親を初期化
this.name = name
子を初期化
サブクラスのオブジェクトを生成すると、親クラスの初期化が終わってから子クラスの初期化が行われます。
1つのコンストラクタの先頭で super()this() を同時には呼べません。どちらも「先頭に書く」必要があるためです。

サンプル2:RectangleとNamedRectangle

次のコードを NamedRectangle.java という名前で保存してください。1ファイルの中に親クラス、子クラス、main メソッドが揃っているため、そのままコンパイルして実行できます。

コードの配色
NamedRectangle.java
// スーパークラス
class Rectangle {
  int width;
  int height;

  Rectangle(int width, int height) {
    this.width = width;
    this.height = height;
    System.out.println("Rectangleを初期化");
  }

  int getArea() {
    return width * height;
  }
}

// サブクラス
public class NamedRectangle extends Rectangle {
  String name;

  NamedRectangle() {
    this("NO NAME", 200, 32);
  }

  NamedRectangle(String name, int width, int height) {
    super(width, height);
    this.name = name;
    System.out.println("NamedRectangleを初期化");
  }

  void printInfo() {
    System.out.println("名前: " + name);
    System.out.println("幅: " + width);
    System.out.println("高さ: " + height);
    System.out.println("面積: " + getArea());
  }

  // NamedRectangleの動作を確認するmainメソッド
  public static void main(String[] args) {
    NamedRectangle rectangle =
        new NamedRectangle("教室の黒板", 180, 90);

    rectangle.printInfo();
  }
}
コンパイルと実行
javac NamedRectangle.java
java NamedRectangle
実行結果を確認する
output
Rectangleを初期化
NamedRectangleを初期化
名前: 教室の黒板
幅: 180
高さ: 90
面積: 16200
NamedRectanglewidthheightgetArea() は定義されていませんが、Rectangle から継承しているため利用できます。

サンプル1:AnimalとDog

次のコードを Dog.java という名前で保存してください。親クラスのメソッドをそのまま使う例と、子クラスでオーバーライドする例を、それぞれ2個ずつ実行して違いを確認できます。

コードの配色
Dog.java
// スーパークラス
class Animal {
  String name;

  Animal(String name) {
    this.name = name;
    System.out.println("Animalを初期化");
  }

  void introduce() {
    System.out.println("名前: " + name);
  }

  void sleep() {
    System.out.println("眠ります");
  }

  void move() {
    System.out.println("動物が移動します");
  }

  void makeSound() {
    System.out.println("動物が鳴きます");
  }
}

// サブクラス
public class Dog extends Animal {
  String sound;
  int soundCount;

  Dog(String name, String sound, int soundCount) {
    super(name);
    this.sound = sound;
    this.soundCount = soundCount;
    System.out.println("Dogを初期化");
  }

  // 親クラスのmove()をオーバーライド
  @Override
  void move() {
    System.out.println("犬が走ります");
  }

  // 親クラスのmakeSound()をオーバーライド
  @Override
  void makeSound() {
    String sounds = "";
    for (int i = 0; i < soundCount; i++) {
      sounds += sound;
    }
    System.out.println(sounds);
  }

  public static void main(String[] args) {
    Dog pochi = new Dog("ポチ", "ワン!", 3);
    Dog shiro = new Dog("シロ", "キャン!", 2);

    System.out.println("--- pochi ---");
    pochi.introduce();  // 継承したメソッド
    pochi.sleep();      // 継承したメソッド
    pochi.move();       // オーバーライドしたメソッド
    pochi.makeSound();  // オーバーライドしたメソッド

    System.out.println("--- shiro ---");
    shiro.introduce();
    shiro.sleep();
    shiro.move();
    shiro.makeSound();
  }
}
コンパイルと実行
javac Dog.java
java Dog
実行結果を確認する
output
Animalを初期化
Dogを初期化
Animalを初期化
Dogを初期化
--- pochi ---
名前: ポチ
眠ります
犬が走ります
ワン!ワン!ワン!
--- shiro ---
名前: シロ
眠ります
犬が走ります
キャン!キャン!
introduce()sleep() は親クラスの処理がそのまま動きます。一方、move()makeSound()Dog でオーバーライドした処理が動きます。soundsoundCount を変えると、犬ごとに鳴き声と鳴く回数を設定できます。

継承は「サブクラスはスーパークラスの一種である」という自然な関係を表します。これをIS-A関係と呼びます。

Animal動物
Bird鳥は動物の一種
Chickenニワトリは鳥の一種
IS-Aの例
class Bird extends Animal {
}

class Chicken extends Bird {
}

あるクラスが別のクラスのオブジェクトをフィールドとして持つ関係をHAS-A関係と呼びます。部品を組み合わせる設計は合成(Composition)です。

Car.java
class Car {
  Engine engine;
  Tire[] tires = new Tire[4];
}
関係読み方設計
Bird — Animal鳥は動物の一種であるIS-A:継承
Car — Engine自動車はエンジンを持つHAS-A:合成
Car — Tire自動車はタイヤを持つHAS-A:合成
単にコードを使い回すためだけに継承するのではなく、自然なIS-A関係が成立するかを確認します。成立しない場合はHAS-Aによる合成を検討しましょう。
  1. スーパークラスの引数付きコンストラクタを呼ぶには何を使いますか。
  2. super() を省略したとき、自動的に呼ばれるのはどのコンストラクタですか。
  3. 「自動車はエンジンを持つ」はIS-AとHAS-Aのどちらですか。
解答を確認する
  1. super(...) をコンストラクタの先頭で使います。
  2. スーパークラスの引数なしコンストラクタ super() です。
  3. HAS-A関係です。通常は継承ではなく合成で表します。