CodeGym /コース /JAVA 25 SELF /super の使い方:基底クラスのコンストラクタとメソッドの呼び出し

super の使い方:基底クラスのコンストラクタとメソッドの呼び出し

JAVA 25 SELF
レベル 17 , レッスン 2
使用可能

1. super を使って基底クラスのメソッドを呼び出す

サブクラスを作るとき、特にメソッドをオーバーライドしたりフィールドを「隠蔽(シャドーイング)」している場合に、基底クラスのフィールドやメソッドにアクセスしたくなることがあります。これを行うために用意されている特別なキーワードが super です。

たとえるなら、サブクラスで親に定義されたものへ明示的にアクセスしたいときの「ママ、助けて!」が super です。

たとえば、Animal クラスに eat() というメソッドがあり、単に "動物は食べる" と出力するとします。Cat クラスでは、猫がまず自分の処理(例:鳴く)を行い、その後に標準の「動物は食べる」も実行したい、といったケースです。ここで役立つのが super.eat() です。

サブクラスでメソッドをオーバーライドしても、その中で基底クラスの実装を呼び出したい場合は、super.メソッド名() を使います。

例:振る舞いを拡張する

class Animal {
    void eat() {
        System.out.println("動物は食べる");
    }
}

class Cat extends Animal {
    @Override
    void eat() {
        System.out.println("猫が餌の匂いを嗅ぐ...");
        super.eat(); // Animal の eat() を呼び出す
        System.out.println("猫は満足そうにゴロゴロ鳴く");
    }
}

どう動く?

  • Cat 型のオブジェクトで eat() を呼ぶと、まずは Cat.eat() のコード、つまり自分自身のメソッドが実行されます。
  • そのメソッドの中で、明示的に super.eat() を呼び、親クラス Animal の実装を実行します。
  • これにより、「親の処理」を残したまま、追加の振る舞いを足せます。

実践:アプリで使ってみる

学習用アプリに、基底クラス Animal とサブクラス DogCat があるとします。給餌時に、共通の処理(例:満腹度の増加)と各動物固有の処理をどちらも実行したい場合です。

class Animal {
    int satiety = 0;

    void eat() {
        satiety += 10;
        System.out.println("動物は食べる。満腹度: " + satiety);
    }
}

class Dog extends Animal {
    @Override
    void eat() {
        System.out.println("犬は食事の前に尻尾を振る");
        super.eat();
    }
}

ここで dog.eat() を呼ぶと、両方のメッセージが表示され、satiety も正しく増加します。

2. super で基底クラスのフィールドにアクセスする

サブクラスで親クラスと同じ名前のフィールドを宣言すると、親のフィールドは「隠蔽(シャドーイング)」されます。元の基底クラスのフィールドへアクセスしたい場合は、super.フィールド名 を使います。

例:フィールドの隠蔽

class Animal {
    String name = "動物";
}

class Cat extends Animal {
    String name = "猫";

    void printNames() {
        System.out.println("Cat の名前: " + name);
        System.out.println("Animal の名前: " + super.name);
    }
}

new Cat().printNames(); の出力:

Cat の名前: 猫
Animal の名前: 動物

実務では、やむを得ない場合を除きフィールドの「隠蔽」は推奨されませんが、こうした手段があることは知っておくべきです。

3. super(...) で基底クラスのコンストラクタを呼ぶ

継承階層でオブジェクトはどう生成される?

サブクラスのオブジェクトを生成するとき、最初に基底クラスのコンストラクタが呼ばれ、その後にサブクラスのコンストラクタが呼ばれます。サブクラスは親の状態を「継承」しているため、すべてのフィールドを正しく初期化する必要があるからです。

基底クラスのコンストラクタを明示的に呼ぶ

親に引数なしコンストラクタがある場合は簡単で、Java がサブクラスのコンストラクタ実行前に自動でそれを呼びます。しかし親に引数なしのコンストラクタがない場合、必要なコンストラクタを super(...) で明示的に呼ぶ義務があります。

例:

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
        System.out.println("動物が作成された: " + name);
    }
}

class Cat extends Animal {
    Cat(String name) {
        super(name); // 必須!引数なしの Animal() コンストラクタは存在しない
        System.out.println("猫が作成された: " + name);
    }
}

new Cat("Murka") を呼ぶと、次のように出力されます:

動物が作成された: Murka
猫が作成された: Murka

重要: 親のコンストラクタ呼び出し(super(...))は、サブクラスのコンストラクタ内で最初の行でなければなりません。これより前に何かを書くと、コンパイラにエラーで注意されます。

明示的に呼ばなかったら?
親にパラメータ付きコンストラクタしかないのに、super(...) を明示的に呼ばなかった場合、コンパイラはエラーを出します:"constructor Animal in class Animal cannot be applied to given types"

4. 便利なニュアンス

いつ super を使うべき?

置き換えではなく拡張したいとき。
メソッドの振る舞いを完全に置き換えるのではなく、親のロジックの前後に何かを追加したい場合があります。そうしたケースでは、オーバーライドしたメソッドの中で super.メソッド名() を使ってください。

継承されたフィールドの初期化のため。
親に初期化必須のフィールド(例:動物の名前)があるなら、必要な引数を渡して super(...) で親のコンストラクタを必ず呼びます。

隠されたフィールド/メソッドへのアクセス。
何らかの理由で親のフィールドやメソッドを「隠蔽」していて、それでも元のものにアクセスしたい場合は、super.フィールド名 または super.メソッド名() を使います。

super の制約と注意点

  • super(...) による親コンストラクタの呼び出しは、コンストラクタの中だけで、しかも最初の行だけに書けます。
  • サブクラスのコンストラクタ以外から親コンストラクタを呼ぶことはできません。
  • super(...) を明示的に呼ばなければ、Java は親の引数なしコンストラクタ(存在する場合)を呼ぼうとします。
  • super は静的メソッドでは使えません。インスタンスメソッドとコンストラクタでのみ使用できます。
  • 親のメソッドやフィールドが private の場合、super ではアクセスできません。プライベートメンバーは不可視のままです。

5. 練習用の例

例 1. super でメソッドを拡張する

class Animal {
    void makeSound() {
        System.out.println("動物が音を出す");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        super.makeSound(); // まず標準の処理を実行
        System.out.println("犬が吠える: ワンワン!");
    }
}

例 2. 基底クラスのコンストラクタ呼び出し

class Vehicle {
    String brand;

    Vehicle(String brand) {
        this.brand = brand;
        System.out.println("乗り物: " + brand);
    }
}

class Car extends Vehicle {
    int year;

    Car(String brand, int year) {
        super(brand); // 親のコンストラクタを呼ぶ
        this.year = year;
        System.out.println("車 " + brand + "、年: " + year);
    }
}
Car car = new Car("Toyota", 2023);
// 出力:
// 乗り物: Toyota
// 車 Toyota、年: 2023

例 3. 典型的なミス:super(...) の呼び出し忘れ

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }
}

class Cat extends Animal {
    Cat() {
        // super(); // エラー!引数なしの Animal() コンストラクタは存在しない
        // super(name) を明示的に呼び出す必要がある
        super("名前のない猫");
    }
}

6. super を使う際のよくある誤り

エラー1: コンストラクタ内の最初の行で super(...) を呼んでいない。
Java は、super(...) による親コンストラクタの呼び出しがサブクラスのコンストラクタの最初の行であることを厳密に要求します。これより前に何か(例:メッセージの出力)を書こうとすると、コンパイルエラーになります。

エラー2: 親に適切なコンストラクタが存在しない。
基底クラスに引数なしコンストラクタがなく、別のコンストラクタを super(...) で呼んでいない場合、コンパイラはデフォルト呼び出しを生成できず、エラーを報告します。

エラー3: super で親のプライベートメンバーにアクセスしようとする。
super は、親のプライベートなフィールドやメソッドへの魔法の鍵ではありません。private として宣言されているものは、サブクラスからは依然としてアクセス不可です。

エラー4: 理解せずにフィールドやメソッドを隠蔽してしまう。
サブクラスで親と同名のフィールドやメソッドを宣言し、それを忘れてしまうと予期しない結果を招くことがあります。その場合、親のメンバーへアクセスするには super を使う必要があることを常に意識してください。

エラー5: 静的メソッド内で super を使う。
静的メソッドでは super を使えません。静的メソッドは特定のオブジェクトに属さないためです。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION