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

Java Java
スポンサーリンク

final フィールドとは何か

final フィールドは「一度だけ代入でき、その後は再代入できない」フィールドです。オブジェクトの生成時に値を確定し、以降は参照を変えないことで、設計の予測可能性が上がります。値オブジェクトや設定スナップショットなど“変わってはいけない前提”を持つ型で特に有効です。


何のために使うのか(重要ポイントの深掘り)

不変条件を守るために使います。生成時に検証して確定すれば、後から書き換えられず整合性が維持されます。並行実行でも「途中で値が変わらない」ため、共有が安全になり、同期やロックの負担を減らせます。equals/hashCode と組み合わせると、ハッシュ構造(HashMap/HashSet)で安全にキーとして使えます。


初期化のルールと“空の final”(blank final)

final は必ず「コンストラクタ内」または「フィールド宣言時」、あるいは「インスタンス初期化子」で一度だけ代入します。宣言時に代入しない final は“空の final”で、全コンストラクタで必ず代入される必要があります。

public final class Money {
    private final int amount;              // 空の final(宣言時未代入)
    private final String currency;         // 空の final

    public Money(int amount, String currency) {
        if (amount < 0) throw new IllegalArgumentException("amount>=0");
        if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");
        this.amount = amount;              // コンストラクタで一度だけ代入
        this.currency = currency.trim();   // ここで確定
    }

    public int amount() { return amount; }
    public String currency() { return currency; }
}
Java

宣言時に代入する方法もあります。設定値や定数など、常に同じ値を持つならこちらが簡潔です。

public final class ConfigDefaults {
    private final int timeoutMs = 3000;    // 宣言時に確定
    public int timeoutMs() { return timeoutMs; }
}
Java

参照は固定でも“中身”は別(重要な注意点)

final は「参照の再代入禁止」であり、「参照先の中身が不変」を保証するものではありません。可変オブジェクトを final にすると、参照は固定でも内部状態は変更できてしまいます。不変を保ちたいなら、内部も不変の型を使うか、防御的コピーで囲います。

public final class Tags {
    private final java.util.List<String> list; // 参照は final

    public Tags(java.util.List<String> input) {
        var xs = (input == null ? java.util.List.<String>of() : input).stream()
            .map(s -> s == null ? "" : s.trim())
            .filter(s -> !s.isEmpty())
            .toList();
        this.list = java.util.List.copyOf(xs); // 変更不可ビューで不変を守る
    }

    public java.util.List<String> asList() { return list; } // 不変ビューのまま返す
}
Java

配列は特に注意が必要です。配列自体の要素は変更できるため、final 配列をそのまま公開しないでください。返却時にコピーしましょう。

public final class Bytes {
    private final byte[] data;
    public Bytes(byte[] input) {
        this.data = (input == null) ? new byte[0] : java.util.Arrays.copyOf(input, input.length);
    }
    public byte[] data() {
        return java.util.Arrays.copyOf(data, data.length); // 防御的コピー
    }
}
Java

static final と“定数”設計

クラス全体で共有する変更不可の値は static final にします。定数は大文字スネークケース(MAX_SIZE など)で表記すると意図が伝わりやすく、JIT の最適化にも親和的です。

public final class MathConstants {
    public static final double PI = 3.141592653589793;
    public static final int DEFAULT_SCALE = 2;
}
Java

インスタンスごとの固定値なら、static は付けません。インスタンス生成時に確定する、という違いを意識してください。


並行性と“安全な公開”(重要ポイントの深掘り)

final フィールドは、コンストラクタで正しく初期化され、コンストラクタの完了後に他スレッドへ公開されれば、他スレッドは常に完全に初期化された値を見ます。可変状態よりも可視性の問題が起きにくく、安全な共有に向いています。並行性のバグを避けたいなら、できる限り final を使い、不変に寄せるのが王道です。


例題で身につける

例 1: 設定スナップショット(不変)

public final class AppConfig {
    private final String region;
    private final int timeoutMs;

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

    public String region() { return region; }
    public int timeoutMs() { return timeoutMs; }

    public AppConfig withTimeout(int ms) { return new AppConfig(region, ms); } // 変更は新インスタンス
}
Java

この形なら、生成後に状態が変わらないため、どこから参照されても安全です。

例 2: 値オブジェクト(等価・ハッシュの一貫性)

import java.util.Objects;

public final class ProductCode {
    private final String value;
    public ProductCode(String raw) {
        var v = raw == null ? "" : raw.trim().toUpperCase();
        if (v.isBlank()) throw new IllegalArgumentException("code required");
        this.value = v;
    }
    @Override public boolean equals(Object o) {
        return o instanceof ProductCode pc && value.equals(pc.value);
    }
    @Override public int hashCode() { return Objects.hash(value); }
    @Override public String toString() { return value; }
}
Java

final によって値が固定され、コレクションのキーとしても安全に扱えます。

例 3: ユーティリティの定数

public final class Urls {
    public static final String HTTP = "http";
    private Urls() {}
    public static boolean isHttp(java.net.URI u) {
        return u != null && HTTP.equalsIgnoreCase(u.getScheme());
    }
}
Java

定数と機能を混ぜず、意図が明確な API になります。


つまずきやすいポイントと回避

final にしても内部の可変オブジェクトは変わる、という罠に注意してください。コレクションや配列は防御的コピーで受け取り・返却を徹底します。空の final は全コンストラクタで必ず代入が必要です。1つでも代入漏れがあるとコンパイルエラーになります。テストや拡張で“差し替えたくなる”場合は、フィールドを final に保ちつつ、外部依存はインターフェース注入に切り替えると柔軟性を維持できます。


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

final フィールドは「生成時に値を確定し、以後変えない」ための道具です。不変条件をコンストラクタで保証し、参照を固定することで、整合性・並行性・読みやすさが大きく向上します。内部が可変になりがちな配列やリストは必ず防御的コピーを使い、定数は static final で明示します。まず“不変に寄せる”方針を徹底すれば、多くのバグの入口を塞げます。

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