Java道|親クラスと子クラスのコンストラクタの関係

子クラスが完成する前に、まず親クラスの土台が作られる。
Javaのコンストラクタの順番を知ると、継承の動きがぐっと立体的に見えてきます。

Javaで継承を使うと、子クラスは親クラスのフィールドやメソッドを受け継げます。
ここで次に大切になるのが、子クラスのオブジェクトを作ったとき、コンストラクタはどの順番で動くのか という点です。

サブクラスのオブジェクトも、通常どおり new を使って作成します。
しかし内部では、サブクラスだけがいきなり作られるわけではありません。

Javaでは、サブクラスのオブジェクトを作るときに、

順番動くもの役割
1スーパークラスのコンストラクタ親クラス側の基本情報を初期化する
2サブクラスのコンストラクタ子クラス側の追加情報を初期化する

という順番で処理が進みます。

鬼滅の刃風にたとえると、柱の隊士が登場するとき、いきなり柱としてだけ完成するわけではありません。
まず鬼殺隊士としての基本情報が整い、そのあとで柱としての担当区域や役割が加わります。

つまり、親の土台が先にできて、その上に子の個性が乗る。
これが、親クラスと子クラスのコンストラクタの基本的な関係です。

↓クリックすると拡大表示されます。

サブクラスのオブジェクトは new で作る

サブクラスのオブジェクト作成は、特別に難しいものではありません。
基本はこれまでと同じように、new を使います。

たとえば、DemonSlayer を継承した PillarSlayer がある場合、次のように書きます。

PillarSlayer slayer1 = new PillarSlayer();

この1行で、PillarSlayer 型のオブジェクトが作成されます。

ただし、このとき作られる slayer1 は、PillarSlayer だけの機能を持つわけではありません。
DemonSlayer から受け継いだ機能も使えます。

鬼滅の刃風にたとえると、柱は柱としての役割を持っていますが、同時に鬼殺隊士でもあります。
そのため、鬼殺隊士としての名前や階級を持ち、さらに柱としての担当区域も持てるわけです。

継承したメソッドと追加したメソッドはどちらも使える

サブクラスのオブジェクトでは、親クラスから受け継いだメソッドと、子クラスで追加したメソッドの両方を呼び出せます。

今回の例では、PillarSlayer オブジェクトから次のメソッドを使えます。

メソッド定義されている場所役割
setSlayer()DemonSlayer名前と階級を設定する
show()DemonSlayer隊士の状態を表示する
setArea()PillarSlayer担当区域を設定する

ここで大切なのは、PillarSlayer の中に setSlayer() や show() を書いていなくても使えることです。
これは、PillarSlayer が DemonSlayer を継承しているからです。

継承とは、親クラスの内容を子クラスに同じように書き直すことではありません。
親クラスの機能を受け継ぎ、必要な追加機能だけを子クラスに書く仕組みです。

クラスの継承を確認する

ファイル名:Sample1.java

class DemonSlayer
{
    private String name;
    private String rank;

    public DemonSlayer()
    {
        name = "隊士未登録";
        rank = "階級未設定";
        System.out.println("鬼殺隊士クラスの隊士を作成しました。");
    }

    public void setSlayer(String n, String r)
    {
        name = n;
        rank = r;
        System.out.println("名前を" + name + "に、階級を" + rank + "にしました。");
    }

    public void show()
    {
        System.out.println("隊士の名前は" + name + "です。");
        System.out.println("階級は" + rank + "です。");
    }
}

class PillarSlayer extends DemonSlayer
{
    private int area;

    public PillarSlayer()
    {
        area = 0;
        System.out.println("柱クラスの隊士を作成しました。");
    }

    public void setArea(int a)
    {
        area = a;
        System.out.println("担当区域を" + area + "にしました。");
    }
}

class Sample1
{
    public static void main(String[] args)
    {
        PillarSlayer slayer1 = new PillarSlayer();

        slayer1.setSlayer("水月", "水柱");
        slayer1.setArea(7);

        slayer1.show();
    }
}

Sample1.java の処理の流れ

まず、main メソッドでは次の行で PillarSlayer オブジェクトを作っています。

PillarSlayer slayer1 = new PillarSlayer();

このとき、見た目は PillarSlayer のオブジェクトだけを作っているように見えます。
しかし、PillarSlayer は DemonSlayer を継承しているため、最初に DemonSlayer 側のコンストラクタが動きます。

処理の順番は次のようになります。

順番処理内容
1DemonSlayer()name と rank を初期化する
2PillarSlayer()area を初期化する
3setSlayer("水月", "水柱")名前と階級を設定する
4setArea(7)担当区域を設定する
5show()名前と階級を表示する

鬼滅の刃風にたとえると、まず鬼殺隊士として登録され、そのあと柱として担当区域を持つ流れです。

実行結果の例

鬼殺隊士クラスの隊士を作成しました。
柱クラスの隊士を作成しました。
名前を水月に、階級を水柱にしました。
担当区域を7にしました。
隊士の名前は水月です。
階級は水柱です。

最初に表示されているのは、DemonSlayer のコンストラクタのメッセージです。

鬼殺隊士クラスの隊士を作成しました。

次に、PillarSlayer のコンストラクタのメッセージが表示されます。

柱クラスの隊士を作成しました。

この順番がとても重要です。

子クラスのオブジェクトを作っているのに、先に親クラスのコンストラクタが動いています。
これは、子クラスが親クラスの土台の上に成り立っているからです。

コンストラクタは継承されない

継承では、親クラスのフィールドやメソッドを子クラスが利用できます。
しかし、コンストラクタは継承されません。

ここは少し混ざりやすいので、表で整理します。

項目継承されるか
フィールド継承の対象になる
メソッド継承の対象になる
コンストラクタ継承されない

ただし、コンストラクタが継承されないからといって、親クラスのコンストラクタが無関係になるわけではありません。

サブクラスのオブジェクトを作るとき、親クラスのコンストラクタは必ず呼び出されます。
つまり、コンストラクタは子クラスに受け継がれるのではなく、子クラスの生成時に親クラス側の初期化として呼び出される、ということです。

何も書かないと super() が自動的に呼ばれる

PillarSlayer のコンストラクタを見てみましょう。

public PillarSlayer()
{
    area = 0;
    System.out.println("柱クラスの隊士を作成しました。");
}

この中には super() が書かれていません。
しかし、Javaではサブクラスのコンストラクタの先頭に何も書かない場合、自動的に親クラスの引数なしコンストラクタが呼ばれます。

つまり、内部的には次のようなイメージです。

public PillarSlayer()
{
    super();
    area = 0;
    System.out.println("柱クラスの隊士を作成しました。");
}

この super() によって、DemonSlayer() が先に動きます。

書いたコードJava内部のイメージ
super() を書いていない自動的に super() が呼ばれる
super(n, r) を書く指定した親コンストラクタを呼ぶ

鬼滅の刃風にたとえると、柱として登録する前に、まず鬼殺隊士としての登録処理が自動的に呼ばれるようなものです。

図:new PillarSlayer() のコンストラクタ順序

↓クリックすると拡大表示されます。

この図が示していること

この図では、new PillarSlayer() を実行したときのコンストラクタの順番を表しています。

最初に DemonSlayer() が動きます。
ここで、鬼殺隊士としての基本情報である name と rank が初期化されます。

そのあとに PillarSlayer() が動きます。
ここで、柱としての追加情報である area が初期化されます。

このように、サブクラスのオブジェクトは、

順番初期化されるもの
1親クラスの部分
2子クラスの部分

という順番で完成します。

super() で親クラスのコンストラクタを指定する

親クラスにコンストラクタが複数ある場合、子クラス側からどの親コンストラクタを呼ぶか指定できます。

そのときに使うのが super() です。

たとえば、DemonSlayer に次の2つのコンストラクタがあるとします。

コンストラクタ内容
DemonSlayer()初期値で鬼殺隊士を作る
DemonSlayer(String n, String r)名前と階級を受け取って鬼殺隊士を作る

PillarSlayer 側で super(n, r) と書くと、DemonSlayer(String n, String r) を呼び出せます。

これにより、子クラスのオブジェクト作成時に、親クラス側の名前と階級もまとめて初期化できます。

親クラスのコンストラクタを確認する

ファイル名:Sample2.java

class DemonSlayer
{
    private String name;
    private String rank;

    public DemonSlayer()
    {
        name = "隊士未登録";
        rank = "階級未設定";
        System.out.println("鬼殺隊士クラスの隊士を作成しました。");
    }

    public DemonSlayer(String n, String r)
    {
        name = n;
        rank = r;
        System.out.println("名前が" + name + "で、階級が" + rank + "の鬼殺隊士を作成しました。");
    }

    public void setSlayer(String n, String r)
    {
        name = n;
        rank = r;
        System.out.println("名前を" + name + "に、階級を" + rank + "にしました。");
    }

    public void show()
    {
        System.out.println("隊士の名前は" + name + "です。");
        System.out.println("階級は" + rank + "です。");
    }
}

class PillarSlayer extends DemonSlayer
{
    private int area;

    public PillarSlayer()
    {
        area = 0;
        System.out.println("柱クラスの隊士を作成しました。");
    }

    public PillarSlayer(String n, String r, int a)
    {
        super(n, r);
        area = a;
        System.out.println("担当区域が" + area + "の柱クラスの隊士を作成しました。");
    }

    public void setArea(int a)
    {
        area = a;
        System.out.println("担当区域を" + area + "にしました。");
    }
}

class Sample2
{
    public static void main(String[] args)
    {
        PillarSlayer slayer1 = new PillarSlayer("水月", "水柱", 7);

        slayer1.show();
    }
}

Sample2.java の処理の流れ

Sample2.java では、次の行で PillarSlayer オブジェクトを作っています。

PillarSlayer slayer1 = new PillarSlayer("水月", "水柱", 7);

このとき、PillarSlayer の引数付きコンストラクタが呼ばれます。

public PillarSlayer(String n, String r, int a)
{
    super(n, r);
    area = a;
    System.out.println("担当区域が" + area + "の柱クラスの隊士を作成しました。");
}

最初に書かれているのが、super(n, r) です。

この super(n, r) は、親クラス DemonSlayer の引数付きコンストラクタを呼び出しています。

public DemonSlayer(String n, String r)
{
    name = n;
    rank = r;
    System.out.println("名前が" + name + "で、階級が" + rank + "の鬼殺隊士を作成しました。");
}

処理の順番を整理すると、次のようになります。

順番処理内容
1super(n, r)親クラス側で名前と階級を初期化する
2area = a子クラス側で担当区域を初期化する
3メッセージ表示柱クラスの隊士として作成完了を表示する
4show()親クラスのメソッドで名前と階級を表示する

鬼滅の刃風にたとえると、水月という隊士が水柱として登録され、そのあと担当区域7を持つ柱として完成する流れです。

Sample2.java の実行結果の例

名前が水月で、階級が水柱の鬼殺隊士を作成しました。
担当区域が7の柱クラスの隊士を作成しました。
隊士の名前は水月です。
階級は水柱です。

この結果を見ると、まず DemonSlayer 側のコンストラクタが動いています。

名前が水月で、階級が水柱の鬼殺隊士を作成しました。

そのあと、PillarSlayer 側のコンストラクタが動いています。

担当区域が7の柱クラスの隊士を作成しました。

ここから、super(n, r) によって親クラスの引数付きコンストラクタが呼び出され、そのあとで子クラスの追加処理が行われていることが分かります。

super() を使うと何が便利なのか

super() を使うと、親クラス側の初期化をオブジェクト作成時にまとめて行えます。

Sample1.java では、いったん引数なしで PillarSlayer を作ってから、あとで setSlayer() を使って名前と階級を設定しました。

PillarSlayer slayer1 = new PillarSlayer();

slayer1.setSlayer("水月", "水柱");

一方、Sample2.java では、オブジェクトを作る時点で名前、階級、担当区域を渡しています。

PillarSlayer slayer1 = new PillarSlayer("水月", "水柱", 7);

この違いを表にすると、次のようになります。

書き方特徴
new PillarSlayer() のあとで setSlayer()作成後に名前と階級を設定する
new PillarSlayer("水月", "水柱", 7)作成時に名前、階級、担当区域を設定する

必須情報を最初から持たせたい場合は、引数付きコンストラクタと super() を使うと自然です。

super() はコンストラクタの先頭に書く

super() には重要なルールがあります。
それは、コンストラクタの先頭に書く必要があるということです。

正しい書き方は次の形です。

public PillarSlayer(String n, String r, int a)
{
    super(n, r);
    area = a;
}

次のように、先に子クラス側の処理を書いてから super() を呼ぶことはできません。

public PillarSlayer(String n, String r, int a)
{
    area = a;
    super(n, r);
}

これは、親クラスの初期化が終わる前に子クラス側の処理を始めることを防ぐためです。

鬼滅の刃風にたとえると、柱として担当区域を決める前に、まず鬼殺隊士としての基本登録を済ませる必要がある、ということです。

図:super() で親コンストラクタを呼ぶ流れ

↓クリックすると拡大表示されます。

この図が示していること

この図では、PillarSlayer の引数付きコンストラクタから super(n, r) を使って、DemonSlayer の引数付きコンストラクタを呼び出す流れを表しています。

まず、DemonSlayer(String n, String r) で name と rank を初期化します。
そのあと、PillarSlayer 側で area を初期化します。

つまり super() は、

役割内容
親クラスのコンストラクタを呼ぶスーパークラス側の初期化を行う
呼び出すコンストラクタを選ぶ引数に合うコンストラクタを指定する
初期化の順番を守る親を先に、子をあとに初期化する

という役割を持っています。

this() と super() の違い

コンストラクタの中では、this() という書き方も出てきます。
this() と super() は似ていますが、呼び出す相手が違います。

記述呼び出すもの
this()同じクラスの別のコンストラクタ
super()スーパークラスのコンストラクタ

鬼滅の刃風にたとえると、this() は同じ PillarSlayer クラス内の別の登録手順を使うことです。
super() は、親である DemonSlayer クラスの登録手順を使うことです。

たとえるなら、次のようになります。

書き方鬼滅の刃風のイメージ
this()柱クラス内の別ルートで登録する
super()鬼殺隊士としての基本登録を行う

this() と super() は同じコンストラクタ内で両方は書けない

this() も super() も、コンストラクタの先頭に書く必要があります。

そのため、同じコンストラクタの中で次のように両方を書くことはできません。

public PillarSlayer(String n, String r, int a)
{
    this();
    super(n, r);
}

どちらも先頭でなければならないため、同時に使えないのです。

Javaは、コンストラクタの最初にどの初期化ルートから始めるのかを1つに決めます。

書き方可能か
先頭に super()可能
先頭に this()可能
this() のあとに super()不可能
super() のあとに this()不可能

親クラスと子クラスのコンストラクタを鬼滅の刃風に整理する

DemonSlayer は、鬼殺隊士としての共通の土台です。
名前や階級のような、どの隊士にも必要な基本情報を持っています。

PillarSlayer は、DemonSlayer を受け継いだ子クラスです。
鬼殺隊士としての基本を持ちながら、柱としての担当区域 area を追加しています。

オブジェクト作成時には、次の順番で処理が進みます。

順番鬼滅の刃風のイメージJavaの処理
1鬼殺隊士として登録するDemonSlayer のコンストラクタ
2柱としての担当区域を加えるPillarSlayer のコンストラクタ
3完成した隊士として使うメソッド呼び出し

親クラスの土台が先に整い、その上に子クラスの追加要素が乗る。
この順番を押さえると、継承とコンストラクタの関係がかなり分かりやすくなります。

押さえておきたいポイント

ポイント内容
サブクラスのオブジェクト作成new で通常どおり作成できる
継承したメソッドサブクラスのオブジェクトから呼び出せる
追加したメソッドサブクラス独自の機能として呼び出せる
コンストラクタの順序先にスーパークラス、次にサブクラス
コンストラクタの継承コンストラクタ自体は継承されない
自動的な super()何も書かない場合、親の引数なしコンストラクタが呼ばれる
明示的な super()親クラスの特定のコンストラクタを呼べる
this()同じクラス内の別コンストラクタを呼ぶ
注意点super() と this() はどちらもコンストラクタの先頭に書く

継承は、親クラスの機能を受け継ぐだけでなく、オブジェクト作成時の初期化の順番にも深く関係しています。

親クラスで共通の土台を整え、子クラスで追加の特徴を加える。
この流れが見えると、サブクラスのオブジェクト作成や super() の意味が自然に理解できるようになります。