Java | オブジェクト指向:フィールド初期化

Java Java
スポンサーリンク

フィールド初期化の全体像

フィールド初期化は「オブジェクトの状態を安全に立ち上げる」ための最初の設計作業です。Java では、フィールドは宣言時の初期化式、インスタンス初期化子、コンストラクタの順で整えられ、未指定なら型ごとのデフォルト値(数値は0、booleanはfalse、参照はnull)が入ります。要は「どこで・何を・どう検証して初期化するか」を決めるのが肝心です。


デフォルト値と初期化の順序

デフォルト値の前提

フィールドは new の直後に、型ごとのデフォルト値が入ります。参照型は null なので「null 前提」の設計にせず、早めに安全な既定値へ正規化する方が無難です。

public class Defaults {
    int n;         // 0
    boolean ok;    // false
    String s;      // null(そのまま使うとNPEの原因)
}
Java

初期化の順序(インスタンス)

new の内部では「メモリ確保 → デフォルト値 → フィールド初期化式 → 親コンストラクタ → 子コンストラクタ」の順序で進みます。これを理解しておくと「未初期化のまま使う」事故を防げます。

class Base {
    protected final String id;
    Base(String id) { this.id = id; }        // 親を先に
}
class Item extends Base {
    private int price = 100;                 // フィールド初期化式
    Item(String id, int price) {
        super(id);                           // 親コンストラクタ
        if (price < 0) throw new IllegalArgumentException();
        this.price = price;                  // 子コンストラクタ
    }
}
Java

フィールド初期化式・初期化子・コンストラクタの使い分け

フィールド初期化式(軽い既定値)

宣言と同時に軽い既定値を入れる場面に最適です。可読性が高く、コンストラクタのノイズも減ります。

public final class Profile {
    private String nickname = "";  // nullを避ける既定値
}
Java

インスタンス初期化子(特殊ケースのみ)

クラス内に「{ … }」で書くインスタンス初期化子は、コンストラクタの前に実行されます。多用すると読みにくくなるので、通常はコンストラクタへ寄せます。

public final class InitExample {
    private final java.util.List<String> tags;
    { tags = new java.util.ArrayList<>(); }  // コンストラクタ共通の前処理が必要なら
    public InitExample() {}
    public InitExample(String first) { tags.add(first); }
}
Java

コンストラクタ(検証+確定の本丸)

必須値の検証、正規化、代入はコンストラクタに集約します。ここで「不正な初期状態を拒否」しておくと、その後のメソッドはシンプルになります。

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");
        this.id = id;
        this.name = name == null ? "" : name.trim().replaceAll("\\s+", " ");
    }
}
Java

static 初期化との違い

クラス全体で共有する初期化

static フィールドは「クラスロード時に一度だけ」初期化されます。定数や設定など、共有の事実に限定すると安全です。

public final class Consts {
    public static final java.nio.charset.Charset UTF8 = java.nio.charset.StandardCharsets.UTF_8; // 即時初期化
    public static final java.util.Locale JP;
    static { JP = java.util.Locale.JAPAN; } // 複雑ならstaticブロックで
}
Java

static を乱用するとテストが難しくなるため、「共有であること」が本質的に正しい場合だけ使います。


final と不変設計、遅延初期化(重要ポイントの深掘り)

final で「一度だけ」を保証

final フィールドはコンストラクタ完了までに一度だけ代入でき、その後は変更不可です。不変は並行性・テストの強力な味方になります。

public final class Token {
    private final String value;
    public Token(String value) {
        if (value == null || value.isBlank()) throw new IllegalArgumentException();
        this.value = value;
    }
    public String value() { return value; }
}
Java

遅延初期化(必要時にだけ用意)

重いオブジェクトは「必要になったら作る」設計も有効です。単一スレッドならシンプルに、並行下では可視性(volatile)や同期を検討します。

public final class Cache {
    private volatile java.util.Map<String, String> map; // 可視性を担保
    public java.util.Map<String, String> get() {
        var m = map;
        if (m == null) {                     // ダブルチェックの簡略形
            synchronized (this) {
                if (map == null) map = new java.util.HashMap<>();
                m = map;
            }
        }
        return m;
    }
}
Java

「とりあえず null」による遅延はNPEの温床です。遅延するなら、初期化と可視性のルールを必ず決めます。


null 安全とコレクションの初期化

null を前提にしない

参照型は null になりがちなので、空文字・空コレクションで早めに正規化します。メソッドの契約(null を受けるかどうか)も明文化しましょう。

public final class Diary {
    private final java.util.List<String> entries = new java.util.ArrayList<>(); // 空から
    public void add(String s) {
        var x = s == null ? "" : s.trim();
        if (!x.isEmpty()) entries.add(x);
    }
}
Java

Optional で「ない」を表す

「値がない」可能性を型で表現すると、初期化と取得の意図が明瞭になります。

import java.util.Optional;

public final class Repo {
    private String owner; // 空文字で初期化でもOK
    public Optional<String> owner() {
        return (owner == null || owner.isBlank()) ? Optional.empty() : Optional.of(owner);
    }
}
Java

継承時の初期化の注意

親→子の順で一貫性を保つ

子のコンストラクタは必ず先頭で super(...) を呼び、親の初期化を完了させます。親に引数なしがない場合、暗黙の super() は使えません。

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

フィールド初期化式とコンストラクタの代入が競合しないよう、役割を整理しておくと読みやすさが上がります。


例題で身につける

例 1: から文字・空コレクションで安全に立ち上げ

public final class Product {
    private final String id;
    private String name = "";                          // null回避
    private final java.util.List<String> tags = new java.util.ArrayList<>();

    public Product(String id, String name) {
        if (id == null || id.isBlank()) throw new IllegalArgumentException("id");
        this.id = id;
        rename(name);
    }
    public void rename(String newName) {
        var x = newName == null ? "" : newName.trim().replaceAll("\\s+", " ");
        if (!x.isEmpty()) this.name = x;
    }
    public void addTag(String tag) {
        var t = tag == null ? "" : tag.trim();
        if (!t.isEmpty()) tags.add(t);
    }
}
Java

例 2: 既定値は初期化式、検証はコンストラクタ

public final class Config {
    private String region = "ap-northeast-1"; // 軽い既定値
    private int timeoutMs = 3000;             // 軽い既定値

    public Config(String region, int timeoutMs) {
        if (timeoutMs <= 0) throw new IllegalArgumentException("timeout>0");
        if (region != null && !region.isBlank()) this.region = region.trim();
        this.timeoutMs = timeoutMs;
    }
}
Java

例 3: 遅延初期化と可視性の確保

public final class LazyFormatter {
    private volatile java.time.format.DateTimeFormatter fmt; // 可視性
    public java.time.format.DateTimeFormatter formatter() {
        var f = fmt;
        if (f == null) {
            synchronized (this) {
                if (fmt == null) fmt = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd");
                f = fmt;
            }
        }
        return f;
    }
}
Java

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

フィールド初期化は「軽い既定値は初期化式」「検証と確定はコンストラクタ」に分けるのが基本。初期化の順序(デフォルト値→初期化式→親→子)を理解し、final で不変を築く。null を前提にせず、空文字・空コレクション・Optional で安全に立ち上げる。static は共有の事実に限定し、遅延初期化を使うなら可視性と同期まで含めて設計する——この型を守れば、状態管理は安定し、後続のコードが驚くほどシンプルになります。

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