Java | オブジェクト指向:親クラスのコンストラクタ呼び出し

Java Java
スポンサーリンク

親コンストラクタ呼び出しとは何か

子クラスのコンストラクタが、親クラス(スーパークラス)のコンストラクタを呼び出して「親の状態を先に正しく初期化する」仕組みです。Java では super(…) を使って明示的に呼びます。親の初期化が終わってから子の初期化に進むのが鉄則で、この順序が崩れると未初期化参照や整合性崩れのバグに直結します。


基本ルールと super(…)

最初の1行で必ず実行される

子コンストラクタで super(…) を書く場合、必ず最初の行で呼ぶ必要があります。もし書かなければ、Java は暗黙に「引数なしの親コンストラクタ super()」を呼ぼうとします。親に引数なしコンストラクタがないのに super() を省略するとコンパイルエラーになります。

class Base {
    final String name;
    Base(String name) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name");
        this.name = name.trim();
    }
}

class User extends Base {
    final boolean active;
    User(String name, boolean active) {
        super(name);      // 先頭行で親を初期化
        this.active = active;
    }
}
Java

this(…) との関係と選び方

子クラス内の別コンストラクタを呼ぶ this(…) と、親を呼ぶ super(…) は両方とも先頭行にしか書けません。つまり同時には使えないため、複数の子コンストラクタを this(…) でまとめ、最終的に一つの“本命コンストラクタ”で super(…) を呼ぶ形に整理すると読みやすく安全です。

class User extends Base {
    final boolean active;
    User(String name) { this(name, true); }      // 別コンストラクタへ委譲
    User(String name, boolean active) {
        super(name);                             // 親初期化はここで一括
        this.active = active;
    }
}
Java

例題で見る検証と初期化の順序

親に不変条件(インバリアント)を閉じる

親が持つ前提(例: name は空でない)を親コンストラクタで保証し、子は super(…) 経由で必ず通過させます。これで「親の状態は常に正しい」が機械的に担保されます。

abstract class Account {
    private final String owner;
    protected Account(String owner) {
        if (owner == null || owner.isBlank()) throw new IllegalArgumentException("owner");
        this.owner = owner.trim();
    }
    public final String owner() { return owner; }
}

final class SavingAccount extends Account {
    private final int rate;
    SavingAccount(String owner, int rate) {
        super(owner);                       // 親の検証を必ず通過
        if (rate < 0 || rate > 100) throw new IllegalArgumentException("rate 0-100");
        this.rate = rate;
    }
}
Java

重要ポイントの深掘り

コンストラクタ中にオーバーライド可能メソッドを呼ばない

親のコンストラクタから、子がオーバーライドする可能性のあるメソッドを呼ぶと、子のフィールドがまだ未初期化のまま参照される危険があります。初期化順序の罠です。

class Base {
    Base() { init(); }              // NG: コンストラクタで呼ぶ
    void init() { /* 子が上書きするかも */ }
}
class Sub extends Base {
    private String name;
    @Override void init() { System.out.println(name.length()); } // name はまだ null
}
Java

回避策は「コンストラクタでオーバーライド可能メソッドを呼ばない」こと。必要なら静的ファクトリやビルダーで初期化ステップを分けます。

デフォルトコンストラクタの仕組み

クラスにコンストラクタを一切書かない場合のみ、引数なしの“暗黙のコンストラクタ”が生成されます。1つでも自前コンストラクタを定義したら、暗黙の引数なしは作られません。親が引数なしを持たないなら、子は必ず super(…) を書いて親の要求に合わせます。

継承チェーンの連鎖

super(…) は多段に連鎖します。最上位の Object まで親→親→…→Object の順に初期化され、そこから子→子→…と降りてきます。これにより「上から下へ、整合性を崩さず」初期化が完了します。


実務指針

親で契約を固定、子は差分だけ

親コンストラクタに不変条件や前処理(正規化・監査開始など)を集約し、子は super(…) を通過したうえで最小限の追加状態だけを初期化します。これが最も壊れにくい形です。

複雑な初期化はファクトリ/ビルダーへ

コンストラクタに分岐や多量の検証を詰め込みすぎると読みづらくなります。可変パラメータや条件付き初期化が増えてきたら、ビルダーやファクトリメソッドに移して、コンストラクタは「必須項目の確定+親初期化」に絞りましょう。


よくあるエラーと回避

親に引数なしコンストラクタがないのに super() を省略

子の先頭行で super(…) を書かなければ、暗黙に super() が呼ばれます。親に引数なしがないとコンパイルエラーになります。必須引数を持つ親なら、必ず super(required…) を明示してください。

this(…) と super(…) を同時に使おうとしてエラー

どちらも先頭行限定なので同時には使えません。this(…) で子内コンストラクタをまとめ、最後に super(…) を呼ぶ構成に整理します。

親の契約をバイパス

親の必須検証や前処理をスキップすると整合性が崩れます。親の契約があるメソッドや初期化は、原則 super(…) 経由で必ず通過させる設計に。


例題で身につける

例 1: this(…) で束ねて super(…) 一括

class Member extends Base {
    private final int level;

    Member(String name) { this(name, 1); }            // 既定レベル
    Member(String name, int level) {
        super(name);                                  // 親の検証を一括で通過
        if (level < 1) throw new IllegalArgumentException("level>=1");
        this.level = level;
    }
}
Java

例 2: 親の検証+子の追加検証

abstract class Document {
    private final String id;
    protected Document(String id) {
        if (id == null || id.isBlank()) throw new IllegalArgumentException("id");
        this.id = id.trim();
    }
    public final String id() { return id; }
}

final class Article extends Document {
    private final String title;
    Article(String id, String title) {
        super(id);                                   // 親の id 検証
        if (title == null || title.isBlank()) throw new IllegalArgumentException("title");
        this.title = title.trim();
    }
}
Java

例 3: 初期化順序の安全化(ファクトリ利用)

final class SecureUser extends Base {
    private final String token;

    private SecureUser(String name, String token) {
        super(name);                                 // 親の契約
        this.token = token;
    }

    static SecureUser of(String name, String rawToken) {
        String token = sanitize(rawToken);           // 前処理は外で
        return new SecureUser(name, token);          // コンストラクタは最小に
    }

    private static String sanitize(String s) { return s == null ? "" : s.trim(); }
}
Java

仕上げのアドバイス(重要部分のまとめ)

親クラスのコンストラクタ呼び出しは「親の契約を先に満たし、整合性を土台から固める」ための要。super(…) は子コンストラクタの先頭行で明示し、this(…) で束ねるなら最終的に一箇所で super(…) を呼ぶ。コンストラクタでオーバーライド可能メソッドを呼ばない、親の不変条件を親で保証する、複雑な初期化はファクトリ/ビルダーに逃がす——この型を守ると、初期化のバグが激減し、継承が安全に機能します。

タイトルとURLをコピーしました