コンストラクタの全体像
コンストラクタは「オブジェクトの初期状態を正しく確立するための特別なメソッド」です。返り値型を書かずクラス名と同名で定義し、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);
}
}
}
Javarecord のコンストラクタ(簡潔な値オブジェクト)
値オブジェクトは 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 で簡潔に——この型を身につければ、生成から壊れにくい設計が自然に書けます。
