フィールド初期化の全体像
フィールド初期化は「オブジェクトの状態を安全に立ち上げる」ための最初の設計作業です。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+", " ");
}
}
Javastatic 初期化との違い
クラス全体で共有する初期化
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ブロックで
}
Javastatic を乱用するとテストが難しくなるため、「共有であること」が本質的に正しい場合だけ使います。
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);
}
}
JavaOptional で「ない」を表す
「値がない」可能性を型で表現すると、初期化と取得の意図が明瞭になります。
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 は共有の事実に限定し、遅延初期化を使うなら可視性と同期まで含めて設計する——この型を守れば、状態管理は安定し、後続のコードが驚くほどシンプルになります。
