Java | オブジェクト指向:static フィールドの用途

Java Java
スポンサーリンク

全体像

static フィールドは「クラスに属する共有の状態」を表します。インスタンス(個体)ごとに違う値ではなく、クラス全体で1つだけ存在する値です。用途は「定数」「設定」「生成数のカウンタ」「共有キャッシュ・リソース」「レジストリやシングルトンの保持」などが中心で、共通の事実に限定すると安全に使えます。乱用するとテスト困難や並行性バグの温床になるため、用途と設計の線引きが重要です。


定数と共有設定(最も安全な使い道)

不変の定数を型で表す

定数は public static final で定義し、意味を持つ名前でコード全体の“魔法の数字”を置き換えます。不変なので並行性の問題が起きません。

public final class Consts {
    private Consts() {}
    public static final int MAX_RETRY = 3;
    public static final java.nio.charset.Charset UTF8 = java.nio.charset.StandardCharsets.UTF_8;
    public static final java.util.Locale JP = java.util.Locale.JAPAN;
}
Java

共有設定(読み取り専用または起動時に確定)

アプリ全体で使う設定は static で持てますが、可能なら不変(final)に。起動時の読み込みで一度だけ確定し、その後は変更しないのが安全です。

public final class AppConfig {
    public static final String REGION = "ap-northeast-1";
    public static final int TIMEOUT_MS = 3000;
}
Java

集計と識別子(生成数・連番の発行)

生成数のカウンタ

クラスの生成数を数えるなど「全体の統計」は static に向いています。更新は競合しやすいのでアトミック型を使うと安全です。

public final class BankAccount {
    private static final java.util.concurrent.atomic.AtomicInteger created = new java.util.concurrent.atomic.AtomicInteger(0);
    private int balance;

    public BankAccount(int initial) {
        if (initial < 0) throw new IllegalArgumentException();
        this.balance = initial;
        created.incrementAndGet();
    }
    public static int createdCount() { return created.get(); }
}
Java

連番やIDの発行

インスタンス間で重複しない連番が欲しいときも static が適任です。並行性に注意して、アトミック+必要なら桁あふれ対策を入れます。

public final class Ids {
    private static final java.util.concurrent.atomic.AtomicLong seq = new java.util.concurrent.atomic.AtomicLong(0);
    public static long next() { return seq.incrementAndGet(); }
}
Java

共有キャッシュ・リソース(重要ポイントの深掘り)

キャッシュ(読み取りが中心、更新は同期)

フォーマッタや正規表現、変換テーブルなど「高コストで作るが読み取りが多い」ものは static で共有すると効率的です。不変にできるなら理想、可変なら同期やスレッド安全なコレクションを使います。

public final class Patterns {
    private Patterns() {}
    public static final java.util.regex.Pattern EMAIL = java.util.regex.Pattern.compile("^[^\\s@]+@[^\\s@]+$");
}
Java

共有リソース(プール・クライアント)

接続プールや HTTP クライアントは static に置くと再利用しやすいですが、ライフサイクルとシャットダウン管理が必須です。try-with-resourcesで毎回生成するより、長寿命の共有クライアントを安全に使う設計が望ましいです。

public final class Http {
    private static final java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient();
    public static java.net.http.HttpResponse<String> get(String url) throws java.io.IOException, InterruptedException {
        var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(url)).GET().build();
        return client.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
    }
}
Java

レジストリとシングルトン(設計の選択肢)

レジストリ(共有の表)

アプリ全体で参照する「名前→オブジェクト」の表を static に持つことがあります。多用するとグローバル依存が増えるため、DI(依存性注入)で置き換えられないかを検討するのが基本です。

public final class Registry {
    private static final java.util.Map<String, String> map = new java.util.concurrent.ConcurrentHashMap<>();
    public static void put(String key, String value) { map.put(key, value); }
    public static String get(String key) { return map.get(key); }
}
Java

シングルトン(唯一の実体)

唯一のインスタンスを static で保持する典型。ただしテストや差し替えに弱くなるため、インターフェース+DIの方が拡張に強い場面が多いです。

public final class Logger {
    private static final Logger INSTANCE = new Logger();
    private Logger() {}
    public static Logger get() { return INSTANCE; }

    public void info(String msg) { System.out.println("[INFO] " + msg); }
}
Java

乱用を避けるための設計指針(重要ポイントの深掘り)

共有 mutable は最小化

mutable な static フィールド(書き換え可能な共有状態)は並行性バグとテスト困難の原因になります。可能な限り不変(final)にし、必要な更新は同期・アトミック型・スレッド安全コレクションで守ります。

ユーティリティは「純粋」に

static フィールドを前提にした隠れ依存を増やさず、ユーティリティは「引数だけに依存して戻り値を返す」純粋さを目指します。副作用が少ないほど安全でテストしやすくなります。

依存の注入で置き換え可能に

本番とテストで差し替えたいもの(ゲートウェイ、ストア、時計など)は、static フィールドに固定しないでコンストラクタ引数経由で渡します。これによりモック化や並列テストが容易になります。


例題で身につける

例 1: 定数とユーティリティの組み合わせ

public final class Strings {
    private Strings() {}
    public static final java.util.Locale LOCALE = java.util.Locale.ROOT;

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

定数(LOCALE)は共有、不変。normalize は引数だけに依存する純粋関数。

例 2: 共有のフォーマッタ(不変で安全)

public final class DateFormats {
    private DateFormats() {}
    public static final java.time.format.DateTimeFormatter ISO =
        java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; // 不変・スレッド安全
}
Java

不変のフォーマッタなら static 共有が適任です。

例 3: 連番発行とID付与

public final class User {
    private static final java.util.concurrent.atomic.AtomicLong seq = new java.util.concurrent.atomic.AtomicLong(0);
    private final long id;
    private final String name;

    public User(String name) {
        this.id = seq.incrementAndGet(); // 共有連番
        this.name = name == null ? "" : name.trim();
    }
    public long id() { return id; }
    public String name() { return name; }
}
Java

IDの重複が起こらないよう、共有の発行元を static で一箇所に。


よくあるつまずきと回避

public static を多用して「どこからでも書き換え可能」な共有状態を作ると、予期せぬ副作用で破綻します。原則として不変(final)で定義し、どうしても mutable が必要なら可視性を絞る(private)+安全な公開メソッドに限定してください。テストで static の隠れ依存に苦しむ場合は、依存をインスタンス化して注入する設計にリファクタリングすると解消します。並行更新を synchronized だけで済ませるより、アトミック型や並行コレクションを使う方が安全でスケールします。


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

static フィールドは「クラス全体で共有する事実」にだけ使うのが基本。最も安全なのは不変の定数で、次に読み取り中心の共有リソース。連番や集計はアトミック型で安全に扱い、レジストリやシングルトンは乱用せず、差し替えが必要な依存はインスタンスに寄せる。共有 mutable を最小化し、可視性と同期で守る——この線引きができると、コードは読みやすく、並行性やテストにも強くなります。

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