Java | オブジェクト指向:コンストラクタの役割

Java Java
スポンサーリンク

コンストラクタの全体像

コンストラクタは「オブジェクトの初期状態を正しく確立するための特別なメソッド」です。返り値型を書かずクラス名と同名で定義し、new とともに呼ばれます。役割は「必要な値を受け取り、検証し、フィールドへ一度だけ正しく代入する」こと。ここで整合性を固めると、その後のメソッドは安全に動きます。


初期化と検証の中心(最初に正しくする)

正しい初期状態を確立する

コンストラクタは「不正な初期状態を拒否」して、以後壊れにくいオブジェクトを作ります。検証は必ずここで行い、ルールに合わない入力は例外で拒否します。

public final class User {
    private final String id;
    private String name;

    public User(String id, String name) {
        if (id == null || id.isBlank()) throw new IllegalArgumentException("id required");
        this.id = id;
        this.name = normalize(name); // 前処理を一箇所に
    }

    public String id() { return id; }
    public String name() { return name; }

    private static String normalize(String s) {
        return s == null ? "" : s.trim().replaceAll("\\s+", " ");
    }
}
Java

「初期化時の検証+前処理」をコンストラクタへ集約すると、他のメソッドが入力の揺れに悩まされなくなります。

不変フィールド(final)と一度だけの代入

final フィールドはコンストラクタ完了までに一度だけ代入できます。以後変更不可になり、並行性やテストでの安全性が高まります。

public final class Token {
    private final String value;
    public Token(String value) {
        if (value == null || value.isBlank()) throw new IllegalArgumentException("token");
        this.value = value; // 以後不変
    }
    public String value() { return value; }
}
Java

初期化の順序(new の裏側を理解する)

new の流れ

new では「メモリ確保→デフォルト初期化(数値0、参照null、boolean false)→フィールド初期化式→親クラスのコンストラクタ→子クラスのコンストラクタ」の順で進みます。これを理解すると「未初期化のまま使う」事故を避けられます。

public class Base {
    protected final String id;
    public Base(String id) { this.id = id; }     // 親の初期化
}
public final class Item extends Base {
    private int price = 0;                       // フィールド初期化式
    public Item(String id, int price) {
        super(id);                               // 先に親
        if (price < 0) throw new IllegalArgumentException("price>=0");
        this.price = price;                      // 子の初期化
    }
}
Java

親の初期化を super(...) で必ず最初に呼ぶのがルールで、これが継承チェーン全体の整合性を守ります。


オーバーロードと共通化(this/super の使い方)

複数の初期化パターンをまとめる

引数違いで複数のコンストラクタを用意する場合、重複処理は this(...) の委譲で一箇所へ集約します。

public final class Email {
    private final String value;

    public Email(String raw) {
        this.value = normalize(raw);
        if (!isValid(this.value)) throw new IllegalArgumentException("invalid email");
    }
    public Email() { this(""); }                 // 既定値に委譲(例として)
    // 共通処理
    private static String normalize(String s) { return s == null ? "" : s.trim().toLowerCase(); }
    private static boolean isValid(String s) { int at = s.indexOf('@'); return at > 0 && at == s.lastIndexOf('@') && at < s.length()-1; }
}
Java

継承時は super(...) で親の初期化を必ず先に行い、子の検証・代入はその後に続けます。


例外方針と失敗の扱い(重要ポイントの深掘り)

何が失敗かを明確にする

コンストラクタは「契約に違反する入力」を即座に例外で拒否します。回復不能なケース(必須値欠落、範囲外など)は IllegalArgumentException が自然です。外部I/Oが絡むなら、チェック例外をスローすることもあります。

public final class Config {
    private final java.util.Properties props;

    public Config(java.nio.file.Path path) throws java.io.IOException {
        try (var in = java.nio.file.Files.newInputStream(path)) {
            var p = new java.util.Properties();
            p.load(in);
            this.props = p;
        }
    }
    public String get(String key) { return props.getProperty(key); }
}
Java

「どんな条件で例外を投げるか」をドキュメントやメソッド名(ofOrThrow など)で伝えると、呼び手のコードが明確になります。


コンストラクタ以外の生成手段(設計の選択肢)

ファクトリメソッドで意図を伝える

複雑な生成規則や名前付きのバリアントがあるなら、static ファクトリを用意すると意図が伝わります。キャッシュや再利用にも向きます。

public final class Money {
    private final int amount;
    private final String currency;
    private Money(int amount, String currency) { this.amount = amount; this.currency = currency; }

    public static Money of(int amount, String currency) {
        if (amount < 0 || currency == null || currency.isBlank()) throw new IllegalArgumentException();
        return new Money(amount, currency);
    }
    public static Money zero(String currency) { return new Money(0, currency); }  // 名前付きバリアント
}
Java

ビルダーパターンで多引数を整理

必須・任意が混在する多引数は、順不同で読みやすいビルダーが有効です。最終的に build() 内で一括検証します。

public final class Mail {
    private final String to, subject, body;
    private Mail(String to, String subject, String body) { this.to = to; this.subject = subject; this.body = body; }

    public static class Builder {
        private String to, subject, body;
        public Builder to(String v) { this.to = v; return this; }
        public Builder subject(String v) { this.subject = v; return this; }
        public Builder body(String v) { this.body = v; return this; }
        public Mail build() {
            if (to == null || to.isBlank()) throw new IllegalStateException("to required");
            return new Mail(to, subject, body);
        }
    }
}
Java

record のコンストラクタ(簡潔な値オブジェクト)

値オブジェクトは record で簡潔に書けます。必要なら「コンパクトコンストラクタ」で検証を追加します。

public record Point(int x, int y) {
    public Point {                      // 引数名と同名でコンパクト定義
        if (x < 0 || y < 0) throw new IllegalArgumentException("non-negative");
    }
}
Java

よくあるつまずきと回避

検証を怠る

「あとで直せばいい」は危険。初期状態が壊れていると、後続のメソッドがすべて危うくなります。必ずコンストラクタ(またはファクトリ)でチェックを入れましょう。

ロジックを詰め込みすぎる

コンストラクタにI/Oや重い計算を大量に入れると、生成が遅く、例外処理も複雑になります。生成は「初期化と検証」に絞り、重い処理は遅延実行や別メソッドへ。

static を乱用する

共有状態に依存して生成する設計は、テストが難しくなりがちです。依存をコンストラクタ引数で注入し、外から差し替え可能にします。

可視性を緩くする

フィールドを public にすると、初期化ルールが簡単に破られます。必ず private にして、必要な値だけゲッター経由で公開します。


例題で身につける

例 1: 安全なユーザー生成(検証+前処理)

public final class User {
    private final String id;
    private final String name;

    public User(String id, String name) {
        if (id == null || id.isBlank()) throw new IllegalArgumentException("id");
        var n = name == null ? "" : name.trim().replaceAll("\\s+", " ");
        this.id = id;
        this.name = n;
    }
    public String id() { return id; }
    public String name() { return name; }
}
Java

例 2: 継承チェーンの初期化

public abstract class Person {
    protected final String name;
    public Person(String name) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name");
        this.name = name.trim();
    }
}
public final class Employee extends Person {
    private final int grade;
    public Employee(String name, int grade) {
        super(name);                        // 先に親
        if (grade < 1 || grade > 10) throw new IllegalArgumentException("grade");
        this.grade = grade;
    }
}
Java

例 3: ファクトリで意図を示す

public final class Url {
    private final java.net.URI uri;
    private Url(java.net.URI uri) { this.uri = uri; }
    public static Url parse(String s) {
        try { return new Url(new java.net.URI(s)); }
        catch (java.net.URISyntaxException e) { throw new IllegalArgumentException("bad url: " + s, e); }
    }
    public java.net.URI toUri() { return uri; }
}
Java

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

コンストラクタは「オブジェクトの最初の整合性を保証する場所」。必須値の検証と前処理を必ず行い、final で不変を守る。初期化順序(フィールド初期化式→super→自分)を理解し、重い処理は詰め込み過ぎない。複雑な生成はファクトリやビルダーへ切り出し、値オブジェクトは record で簡潔に——この型を身につければ、生成から壊れにくい設計が自然に書けます。

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