Javaのstaticとは、メンバー(変数やメソッド)を「個々のオブジェクト」ではなく「クラスそのもの」に属させる修飾子です。 staticを付けた変数はクラスに1つだけ存在して全オブジェクトで共有され、staticを付けたメソッドはnewせずにクラス名.メソッド名()で呼び出せます。

この記事では、staticを付けられる5つの場所(変数・メソッド・初期化ブロック・ネストクラス・import)を一覧で整理し、インスタンス変数との違いをカウンターの例でOpenJDK 21.0.10で実行して確認します。よく出会うコンパイルエラー「non-static variable cannot be referenced from a static context」の全文と3通りの直し方、static初期化ブロックの実行順序、staticネストクラスと内部クラスの違い、さらに可変のstatic変数で起きるスレッド競合を実際に再現し、使うべき場面・避けたい場面をチェックリストにまとめました。

Java staticの早見表

付ける場所 書き方の例 意味 呼び出し・使い方
static変数(クラス変数) static int count; クラスに1つだけ。全インスタンスで共有 クラス名.count
static定数 static final int MAX = 100; 変更できない共有の値 クラス名.MAX
staticメソッド(クラスメソッド) static int max(int a, int b) インスタンスなしで呼べる。thisは使えない クラス名.max(1, 2)
static初期化ブロック static { ... } クラスの初期化時に1回だけ実行 自動で実行される
staticネストクラス static class Item { } 外側のインスタンスなしで作れる入れ子のクラス new Outer.Item()
static import import static java.lang.Math.max; クラス名を省いて呼べる max(1, 2)
比較項目 static(クラスに属する) インスタンス(オブジェクトに属する)
個数 クラスに1つ newした数だけ
呼び出し クラス名.メンバー 変数名.メンバー
thisの使用 使えない 使える
相手側のメンバーへのアクセス インスタンスのメンバーは直接は使えない staticのメンバーを使える
初期化のタイミング クラスの初期化時に1回 newのたび
向いているもの 定数、ユーティリティ、ファクトリ オブジェクトごとに違う値と、それを使う処理

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java staticとは|static変数・メソッドの使い方とエラー例」

動作確認した環境

この記事のコードは、OpenJDK 21.0.10でjava -Dstdout.encoding=UTF-8 ファイル名.java(単一ファイル実行、外部ライブラリなし)として実行し、出力をそのまま載せています。 コンパイルエラーはjavacで確認し、英語表示(-J-Duser.language=en)で掲載しています。Java 25の変更点は、この環境では動かせないため公式情報で確認した内容として注記しています。

static変数とインスタンス変数の違い(カウンターで確認)

static変数は全員で1つを共有し、インスタンス変数はオブジェクトごとに別々に持ちます。 会員(Member)のログイン回数を例に、違いを確認します。

public class CounterDemo {
    public static void main(String[] args) {
        Member a = new Member("佐藤");
        Member b = new Member("鈴木");
        Member c = new Member("高橋");
        a.login(); a.login(); b.login();

        System.out.println(a.name + " のログイン回数(インスタンス変数): " + a.loginCount);
        System.out.println(b.name + " のログイン回数(インスタンス変数): " + b.loginCount);
        System.out.println(c.name + " のログイン回数(インスタンス変数): " + c.loginCount);
        System.out.println("作成した会員数(static変数): " + Member.createdCount);
        System.out.println("全員のログイン合計(static変数): " + Member.totalLogins);
        System.out.println("a.createdCount と書いても同じ値: " + a.createdCount);
    }
}

class Member {
    static int createdCount = 0;   // クラスに1つ:全インスタンスで共有
    static int totalLogins = 0;
    final String name;             // インスタンスごと
    int loginCount = 0;            // インスタンスごと

    Member(String name) {
        this.name = name;
        createdCount++;
    }

    void login() {
        loginCount++;
        totalLogins++;
    }
}

実行結果(OpenJDK 21.0.10):

佐藤 のログイン回数(インスタンス変数): 2
鈴木 のログイン回数(インスタンス変数): 1
高橋 のログイン回数(インスタンス変数): 0
作成した会員数(static変数): 3
全員のログイン合計(static変数): 3
a.createdCount と書いても同じ値: 3

loginCountは会員ごとに2・1・0と別の値、createdCountとtotalLoginsは全員で共有された1つの値になりました。インスタンスメソッドのlogin()からは、インスタンス変数とstatic変数の両方を使えることもわかります。

a.createdCount のような書き方は警告対象

static変数はa.createdCountのようにインスタンス経由でも読めますが、Member.createdCountと書くのが基本です。 インスタンス経由だと「その会員の値」に見えてしまうためです。javac -Xlint:staticを付けると警告が出ます。

CounterDemo.java:13: warning: [static] static variable should be qualified by type name, Member, instead of by an expression
        System.out.println("a.createdCount と書いても同じ値: " + a.createdCount);
                                                          ^
1 warning

メモリ上のイメージ

static変数はクラスの情報といっしょに1つだけ置かれ、インスタンス変数はnewしたオブジェクトの中にそれぞれ置かれる、とイメージすると理解しやすくなります。

flowchart LR
    subgraph CLASS["Member クラス(1つだけ)"]
        S1["static createdCount = 3"]
        S2["static totalLogins = 3"]
    end
    subgraph HEAP["new で作ったオブジェクト"]
        A["a: name=佐藤<br/>loginCount=2"]
        B["b: name=鈴木<br/>loginCount=1"]
        C["c: name=高橋<br/>loginCount=0"]
    end
    A -. 共有して参照 .-> CLASS
    B -. 共有して参照 .-> CLASS
    C -. 共有して参照 .-> CLASS

※図は「クラスに1つ」という論理的な関係を表したものです。実際の配置はJVMの実装によります(現在のHotSpot JVMでは、static変数はクラスを表すjava.lang.Classオブジェクトとともにヒープ上に置かれます)。

「non-static variable cannot be referenced from a static context」の原因と直し方

staticメソッドの中から、インスタンス変数やインスタンスメソッドを直接使うと、このコンパイルエラーになります。 mainメソッドはstaticなので、初めてJavaを書くときによく出会うエラーです。

public class StaticError {
    int count = 0;

    void increment() {
        count++;
    }

    public static void main(String[] args) {
        count = 10;
        increment();
        System.out.println(this.count);
    }
}

javac StaticError.javaの結果(OpenJDK 21.0.10、英語表示):

StaticError.java:9: error: non-static variable count cannot be referenced from a static context
        count = 10;
        ^
StaticError.java:10: error: non-static method increment() cannot be referenced from a static context
        increment();
        ^
StaticError.java:11: error: non-static variable this cannot be referenced from a static context
        System.out.println(this.count);
                           ^
3 errors

日本語ロケールではエラー: staticでない変数 countをstaticコンテキストから参照することはできませんと表示されました(同じJDKで確認)。

なぜエラーになるのか

staticメソッドは「どのオブジェクトのcountか」がわからない状態で呼ばれるからです。上の会員の例でいえば、a・b・cのどのloginCountを指すのかが決まりません。staticメソッドにはthis(今のオブジェクト)が渡されないため、this.countも同じ理由でエラーになります。

エラー文 意味
non-static variable count cannot be referenced from a static context staticの中からインスタンス変数countを使った
non-static method increment() cannot be referenced from a static context staticの中からインスタンスメソッドを呼んだ
non-static variable this cannot be referenced from a static context staticの中でthisを使った(内部クラスのnewでも出る。後述)

3通りの直し方

どれを選ぶかは「その値は全員で1つなのか、オブジェクトごとなのか」で決めます。

public class FixDemo {
    // 直し方1:本当に共有したい値なら static にする
    static int sharedCount = 0;
    static void incrementShared() { sharedCount++; }

    // 直し方2・3 で使うインスタンス変数とメソッド
    int count = 0;
    void increment() { count++; }

    // 直し方3:処理を static メソッドにしたいなら、対象を引数で受け取る
    static void incrementTwice(FixDemo target) {
        target.increment();
        target.increment();
    }

    public static void main(String[] args) {
        // 1. static にする
        incrementShared();
        System.out.println("1. static にする        : sharedCount = " + sharedCount);

        // 2. インスタンスを作ってから使う
        FixDemo app = new FixDemo();
        app.increment();
        System.out.println("2. インスタンスを作る   : app.count = " + app.count);

        // 3. インスタンスを引数で渡す
        FixDemo other = new FixDemo();
        incrementTwice(other);
        System.out.println("3. 引数で渡す           : other.count = " + other.count);
    }
}

実行結果(OpenJDK 21.0.10):

1. static にする        : sharedCount = 1
2. インスタンスを作る   : app.count = 1
3. 引数で渡す           : other.count = 2
直し方 向いている場面 注意点
1. 変数・メソッドをstaticにする 定数や、本当に全体で1つでよい値 エラーを消すためだけに付けると、共有状態が増える(後述のスレッド競合・テストの問題)
2. newしてインスタンス経由で使う ほとんどの場合の第一候補 mainからは「オブジェクトを作って処理を任せる」形にする
3. 対象を引数で受け取る ユーティリティとして処理だけ共通化したい 引数が増えすぎたらインスタンスメソッドに戻す

エラーが出たら、まず2(インスタンスを作る)を検討するのがおすすめです。mainには「オブジェクトを組み立てて最初の処理を呼ぶ」だけを書き、処理本体はインスタンスメソッドに置くと、staticの範囲が小さく保てます。

static初期化ブロックの実行順序

static初期化ブロック(static { ... })は、クラスが初期化されるときに1回だけ、書いた順に実行されます。 インスタンス初期化ブロックやコンストラクタとの順番を確かめます。

public class InitOrder {
    public static void main(String[] args) {
        System.out.println("main 開始");
        System.out.println("Config.VERSION を参照 → " + Config.VERSION);   // コンパイル時定数:クラスは初期化されない
        System.out.println("Config.loadedAt を参照 → " + Config.loadedAt); // ここで初めて static 初期化
        System.out.println("--- 1つ目の new ---");
        new Config();
        System.out.println("--- 2つ目の new ---");
        new Config();
    }
}

class Config {
    static final String VERSION = "1.0";          // コンパイル時定数
    static String loadedAt = log("static フィールド初期化");

    static {
        log("static 初期化ブロック①");
    }
    static {
        log("static 初期化ブロック②(書いた順に実行)");
    }

    String name = log("インスタンスフィールド初期化");

    {
        log("インスタンス初期化ブロック");
    }

    Config() {
        log("コンストラクタ");
    }

    static String log(String msg) {
        System.out.println("  " + msg);
        return msg;
    }
}

実行結果(OpenJDK 21.0.10):

main 開始
Config.VERSION を参照 → 1.0
  static フィールド初期化
  static 初期化ブロック①
  static 初期化ブロック②(書いた順に実行)
Config.loadedAt を参照 → static フィールド初期化
--- 1つ目の new ---
  インスタンスフィールド初期化
  インスタンス初期化ブロック
  コンストラクタ
--- 2つ目の new ---
  インスタンスフィールド初期化
  インスタンス初期化ブロック
  コンストラクタ

出力からわかる3つのルール

  1. クラスの初期化は「初めて使われたとき」に1回だけ。 main開始時点ではまだ実行されず、2回目のnewでも再実行されません。
  2. static finalの文字列・数値の定数(コンパイル時定数)を読むだけでは、クラスは初期化されません。 Config.VERSIONを参照した時点では何も出力されず、Config.loadedAtを参照した時点で初めて初期化されました。これはJava言語仕様(JLS 12.4.1)で決まっている動きです。
  3. staticフィールドの初期化とstaticブロックは、ソースに書いた順に実行されます。インスタンス側も同様に、フィールド初期化→初期化ブロック→コンストラクタ本体の順でした。
実行される処理 タイミング 回数
staticフィールド初期化・staticブロック クラスが初めて使われたとき(書いた順) 1回だけ
インスタンスフィールド初期化・インスタンス初期化ブロック newのたび(書いた順) newした回数
コンストラクタ本体 上の2つのあと newした回数

staticブロックは、複数行の処理が必要な定数の準備(対応表のMapを組み立てるなど)に向いています。 ただし、staticブロックで例外が起きるとクラスの初期化自体が失敗し(ExceptionInInitializerError)、そのクラスが使えなくなります。ファイル読み込みのように失敗しうる処理は、staticブロックではなく、必要になったときに呼ぶメソッドに置くと扱いやすくなります。

staticネストクラスと内部クラスの違い

クラスの中に書くクラスのうち、staticを付けたものを「staticネストクラス」、付けないものを「内部クラス(インナークラス)」と呼びます。 違いは「外側のオブジェクトとつながっているかどうか」です。

public class NestedDemo {
    public static void main(String[] args) {
        // static ネストクラス:外側のインスタンスなしで作れる
        Order.Item item = new Order.Item("ペン", 3);
        System.out.println("static ネストクラス: " + item);

        // 内部クラス(非static):外側のインスタンスが必要
        Order order = new Order("A-001");
        Order.Tracker tracker = order.new Tracker();
        tracker.mark("出荷準備");
        System.out.println("内部クラス: " + tracker.status());

        // 外側のインスタンスへの参照を内部クラスが持っていることを確認
        System.out.println("Tracker のフィールド: " + java.util.Arrays.toString(
            java.util.Arrays.stream(Order.Tracker.class.getDeclaredFields()).map(f -> f.getName()).toArray()));
        System.out.println("Item のフィールド   : " + java.util.Arrays.toString(
            java.util.Arrays.stream(Order.Item.class.getDeclaredFields()).map(f -> f.getName()).toArray()));
    }
}

class Order {
    private final String orderNo;
    Order(String orderNo) { this.orderNo = orderNo; }

    /** static ネストクラス:外側の static メンバーだけ使える */
    static class Item {
        private final String name;
        private final int qty;
        Item(String name, int qty) { this.name = name; this.qty = qty; }
        @Override public String toString() { return name + " x" + qty; }
    }

    /** 内部クラス:外側のインスタンス(orderNo)を直接使える */
    class Tracker {
        private String last = "未着手";
        void mark(String s) { last = s; }
        String status() { return orderNo + " は " + last; }
    }
}

実行結果(OpenJDK 21.0.10):

static ネストクラス: ペン x3
内部クラス: A-001 は 出荷準備
Tracker のフィールド: [last, this$0]
Item のフィールド   : [name, qty]

内部クラスTrackerには、書いていないthis$0というフィールドがあります。 これはjavacが自動で追加した「外側のOrderオブジェクトへの参照」です。このおかげでorderNoを直接使えますが、内部クラスのオブジェクトが残っている間は外側のオブジェクトも残り続けます。

比較項目 staticネストクラス 内部クラス(非static)
作り方 new Order.Item(...) order.new Tracker()(外側のインスタンスが必要)
外側のインスタンス変数 使えない 使える
外側への隠れた参照 なし あり(実行結果のthis$0)
staticメンバーを宣言 できる Java 16以降はできる(JEP 395で緩和)
向いている用途 ビルダー、DTO、外側と独立した部品 外側の状態を読み書きする補助オブジェクト

外側のインスタンス変数を使わないなら、staticネストクラスにするのがおすすめです。不要な参照を持たないぶん、外側のオブジェクトを必要以上に長く保持せずに済みます(『Effective Java』でも推奨されている考え方です)。

ネストクラスまわりのエラー

public class InnerError {
    private int value = 1;

    class Inner { }

    static class Nested {
        int read() { return value; }
    }

    public static void main(String[] args) {
        Inner in = new Inner();
    }
}
InnerError.java:7: error: non-static variable value cannot be referenced from a static context
        int read() { return value; }
                            ^
InnerError.java:11: error: non-static variable this cannot be referenced from a static context
        Inner in = new Inner();
                   ^
2 errors

static mainの中で内部クラスをnew Inner()すると、「thisを参照できない」というエラーになります。 内部クラスを作るには外側のインスタンス(this)が必要だからです。new InnerError().new Inner()と書くか、外側の値を使わないならstatic class Innerに変えると解消します。

static import でクラス名を省略する

import staticを使うと、staticメソッドや定数をクラス名なしで呼べます。

import static java.lang.Math.max;
import static java.lang.Math.PI;
import static java.util.Map.entry;

import java.util.Map;

public class StaticImportDemo {
    public static void main(String[] args) {
        System.out.println("max(3, 7) = " + max(3, 7));          // Math.max と書かなくてよい
        System.out.printf("PI = %.5f%n", PI);
        Map<String, Integer> stock = Map.ofEntries(entry("ペン", 10), entry("ノート", 5));
        System.out.println("在庫: " + new java.util.TreeMap<>(stock));
    }
}

実行結果(OpenJDK 21.0.10):

max(3, 7) = 7
PI = 3.14159
在庫: {ノート=5, ペン=10}

テストのassertEquals(JUnit)やMap.entry、Collectors.toListのように、何度も出てくる名前に使うと読みやすくなります。 一方、import static java.lang.Math.*;のように多くを取り込むと、どのクラスのメソッドかがわかりにくくなるため、使う名前を個別に書くのがおすすめです。

staticメソッドはオーバーライドされない(隠ぺい)

staticメソッドは、子クラスで同じ名前のメソッドを書いてもオーバーライドされません。 変数の型(宣言した型)でどちらが呼ばれるかが決まります。これを「隠ぺい(hiding)」と呼びます。

public class HidingDemo {
    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println("インスタンスメソッド p.hello()  : " + p.hello());
        System.out.println("static メソッド    p.create() : " + p.create());
        System.out.println("Child.create()                : " + Child.create());
    }
}

class Parent {
    String hello() { return "Parent"; }
    static String create() { return "Parent.create"; }
}

class Child extends Parent {
    @Override String hello() { return "Child"; }
    static String create() { return "Child.create"; }   // オーバーライドではなく「隠す(hiding)」
}

実行結果(OpenJDK 21.0.10):

インスタンスメソッド p.hello()  : Child
static メソッド    p.create() : Parent.create
Child.create()                : Child.create

中身がChildでも、Parent型の変数から呼んだstaticメソッドはParent.createになりました。staticメソッドに@Overrideを付けるとerror: static methods cannot be annotated with @Overrideというコンパイルエラーになる(Java 21で確認)ので、意図しない隠ぺいに気づく手がかりになります。staticメソッドはクラス名.メソッド()の形で呼ぶと、この混乱は起きません。

使うべき場面:定数・ユーティリティ・ファクトリ

staticが向いているのは「状態を持たない」か「変わらない」ものです。 代表的な3パターンを1つのファイルで確認します。

import java.util.Objects;

public class GoodUses {
    public static void main(String[] args) {
        // 1. 定数
        System.out.println("上限: " + Limits.MAX_TASKS);
        // 2. ユーティリティ(状態を持たない処理)
        System.out.println("slug: " + TextUtils.toSlug("  Hello Java World "));
        // 3. static ファクトリ
        Quantity q1 = Quantity.of(5);
        Quantity q2 = Quantity.of(5);
        Quantity zero = Quantity.zero();
        System.out.println("q1=" + q1 + " / zero=" + zero + " / zeroは同じオブジェクト? " + (zero == Quantity.zero()));
        System.out.println("q1.equals(q2)? " + q1.equals(q2));
        try {
            Quantity.of(-1);
        } catch (IllegalArgumentException e) {
            System.out.println("Quantity.of(-1) -> " + e.getMessage());
        }
    }
}

final class Limits {
    private Limits() {}                              // インスタンス化させない
    static final int MAX_TASKS = 100;                // 定数:static final+大文字
}

final class TextUtils {
    private TextUtils() {}
    static String toSlug(String s) {                 // 引数だけで結果が決まる
        return s.strip().toLowerCase().replaceAll("\\s+", "-");
    }
}

final class Quantity {
    private static final Quantity ZERO = new Quantity(0);   // 使い回すインスタンス
    private final int value;
    private Quantity(int value) { this.value = value; }     // new は外から呼ばせない

    static Quantity of(int value) {                         // 名前つきの生成メソッド
        if (value < 0) throw new IllegalArgumentException("数量は0以上: " + value);
        return value == 0 ? ZERO : new Quantity(value);
    }
    static Quantity zero() { return ZERO; }

    @Override public boolean equals(Object o) { return o instanceof Quantity q && q.value == value; }
    @Override public int hashCode() { return Objects.hash(value); }
    @Override public String toString() { return "Quantity(" + value + ")"; }
}

実行結果(OpenJDK 21.0.10):

上限: 100
slug: hello-java-world
q1=Quantity(5) / zero=Quantity(0) / zeroは同じオブジェクト? true
q1.equals(q2)? true
Quantity.of(-1) -> 数量は0以上: -1
パターン 書き方のポイント 標準ライブラリの例
定数 static final+大文字の名前。変更できない値だけ Integer.MAX_VALUE、Math.PI
ユーティリティ 引数だけで結果が決まる処理。privateコンストラクタでインスタンス化を防ぐ Math.max()、Objects.equals()
staticファクトリ of・fromなどの名前で生成。検査や同じオブジェクトの使い回しができる List.of()、Integer.valueOf()、LocalDate.of()

避けたい場面:可変の共有状態・テストのしにくさ・スレッド競合

staticで注意が必要なのは、「書き換えられる値」をstaticに置いたときです。 全員で1つを共有するため、ある処理での変更が、関係のない別の処理にまで影響します。ここでは2つの例を実行で確かめます。

スレッド競合:static int の ++ で数がずれる

4つのスレッドが、static変数をそれぞれ10万回ずつ++します。 期待値は40万です。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class RaceDemo {
    static int unsafeCount = 0;                               // 共有される可変の static 変数
    static final AtomicInteger safeCount = new AtomicInteger();
    static int syncCount = 0;

    static synchronized void incrementSync() { syncCount++; }

    public static void main(String[] args) throws InterruptedException {
        int threads = 4, perThread = 100_000;
        for (int round = 1; round <= 3; round++) {
            unsafeCount = 0; safeCount.set(0); syncCount = 0;
            List<Thread> list = new ArrayList<>();
            for (int t = 0; t < threads; t++) {
                Thread th = new Thread(() -> {
                    for (int i = 0; i < perThread; i++) {
                        unsafeCount++;                 // 読み取り→加算→書き込み が分かれている
                        safeCount.incrementAndGet();   // 1つの不可分な操作
                        incrementSync();               // 1スレッドずつ実行
                    }
                });
                list.add(th);
                th.start();
            }
            for (Thread th : list) th.join();
            System.out.printf("%d回目: 期待値=%d / static int++=%d / AtomicInteger=%d / synchronized=%d%n",
                round, threads * perThread, unsafeCount, safeCount.get(), syncCount);
        }
    }
}

実行結果の一例(OpenJDK 21.0.10、2コアの環境):

1回目: 期待値=400000 / static int++=398128 / AtomicInteger=400000 / synchronized=400000
2回目: 期待値=400000 / static int++=398965 / AtomicInteger=400000 / synchronized=400000
3回目: 期待値=400000 / static int++=399839 / AtomicInteger=400000 / synchronized=400000

static intの++は毎回40万に届かず、値も実行のたびに変わりました。 count++は「読み取る→1足す→書き込む」の3つの手順に分かれていて、2つのスレッドが同じ値を読んでから書き込むと、片方の加算が失われるためです(ずれる数は実行環境やタイミングで変わります)。AtomicIntegerやsynchronizedを使うと、毎回40万になりました。

方法 結果 使いどころ
static int+++ 毎回ずれる 複数スレッドからは使わない
AtomicInteger.incrementAndGet() 正確 カウンター1つならこれが手軽
synchronizedメソッド 正確 複数の変数をまとめて更新したいとき
そもそもstaticにしない ― スレッドごと・リクエストごとの値はインスタンスや引数で持つ

Webアプリケーションのように、1つのクラスが多くのリクエストから同時に使われる環境では、リクエストごとの値(ログイン中の会員、入力値など)をstatic変数に置かないことが大切です。

テストのしにくさ:static の状態が次のテストに残る

static変数の値はプログラムが動いている間ずっと残るため、テストの実行順で結果が変わることがあります。

import java.util.ArrayList;
import java.util.List;

public class TestabilityDemo {
    public static void main(String[] args) {
        // テストA と テストB を順に動かしたつもり
        testA();
        testB();
        System.out.println("--- 順番を入れ替えると ---");
        TaskStore.tasks.clear();
        testB();
        testA();
    }

    static void testA() {
        TaskStore.add("資料作成");
        System.out.println("testA: 件数=" + TaskStore.tasks.size() + (TaskStore.tasks.size() == 1 ? " OK" : " NG(期待値1)"));
    }

    static void testB() {
        TaskStore.add("会議予約");
        TaskStore.add("備品発注");
        System.out.println("testB: 件数=" + TaskStore.tasks.size() + (TaskStore.tasks.size() == 2 ? " OK" : " NG(期待値2)"));
    }
}

class TaskStore {
    static final List<String> tasks = new ArrayList<>();   // final でも中身は変えられる
    static void add(String t) { tasks.add(t); }
}

実行結果(OpenJDK 21.0.10):

testA: 件数=1 OK
testB: 件数=3 NG(期待値2)
--- 順番を入れ替えると ---
testB: 件数=2 OK
testA: 件数=3 NG(期待値1)

先に動いた方だけが成功し、後の方は前のテストの値が残っていて失敗しました。 また、static finalでも参照先のListの中身は変更できる点に注意が必要です。TaskStoreを普通のクラスにしてnew TaskStore()を各テストで作る(またはコンストラクタで受け取る)ようにすると、テストごとに独立した状態で確かめられます。

static を使うかどうかのチェックリスト

チェック項目 はい → いいえ →
値が変わらない定数か? static finalにする 次へ
処理が引数だけで結果を決められるか(状態を使わない)? staticメソッドでよい インスタンスメソッドにする
生成に名前や検査、使い回しを持たせたいか? staticファクトリ(ofなど) コンストラクタでよい
外側のインスタンス変数を使わない入れ子のクラスか? staticネストクラス 内部クラス
書き換えられる値を全体で共有しようとしていないか? インスタンスや引数で持つか、Atomic系・synchronizedで保護 ―
複数スレッド・複数リクエストから同時に使われるか? static変数に状態を置かない ―
テストで値を差し替えたい処理か(時刻、外部接続など)? インスタンスにして外から渡す staticでもよい
エラーを消すためだけにstaticを付けていないか? インスタンスを作る直し方を検討 ―

Java 25 での関連する変更(公式で確認)

Java 25では、JEP 512「Compact Source Files and Instance Main Methods」が正式機能になりました。 OpenJDKの公式ページと各種解説によると、mainメソッドはpublic static void main(String[] args)である必要がなくなり、次のようにstaticなしのvoid main()でも実行できます(この記事の検証環境はJava 21のため未実行です)。

// Java 25 以降(JEP 512)。Java 21 ではこの形は正式機能ではありません
void main() {
    IO.println("Hello");
}

インスタンスのmainから始められるため、初めてJavaを書くときに「non-static ... from a static context」のエラーに出会う機会が減ることが期待されます。ただし、ライブラリやWebアプリケーションの多くでは引き続きpublic static void mainが使われており、この記事で扱ったstaticの考え方はそのまま必要です。Java 21では、同様の機能はJEP 445としてプレビュー(--enable-previewが必要)の扱いでした。

よくある質問

Q. staticメソッドの中で、同じクラスのインスタンスメソッドを呼ぶにはどうしますか? A. newでインスタンスを作り、そのインスタンス経由で呼びます。 例えばnew FixDemo().increment()です。直接increment()と書くとnon-static method increment() cannot be referenced from a static contextになります。

Q. static変数はいつ初期化され、いつ消えますか? A. クラスが初めて使われたときに1回だけ初期化されます(この記事の実行結果で、main開始後に初めて参照した時点で初期化されることを確認しました)。そのクラスがアンロードされるまで残るため、通常のアプリケーションではプログラムが動いている間ずっと残ると考えてよいです。

Q. staticメソッドはインスタンスメソッドより速いので、積極的に使うべきですか? A. 速さを理由に選ぶ必要はほとんどありません。 呼び出しの差はJVMの最適化(インライン化など)でほぼ見えなくなることが多く、選ぶ基準は「状態を使うかどうか」です。状態を使わない処理はstatic、使う処理はインスタンスメソッドにすると、読みやすく、テストもしやすくなります。

Q. シングルトンにstaticを使うのはよい方法ですか? A. 1つだけのインスタンスをstaticフィールドで持つ書き方(シングルトン)は広く使われていますが、状態を持つ場合はテストで差し替えにくくなります。Spring BootなどのDIコンテナを使う場合は、Beanのスコープ(既定はシングルトン)に任せて、通常のクラスとして書くのが一般的です。

まとめ

  • staticは「クラスに属する」印。static変数は全インスタンスで1つを共有し、staticメソッドはクラス名.メソッド()で呼べる
  • 付けられる場所は変数・メソッド・初期化ブロック・ネストクラス・importの5つ
  • 「non-static ... cannot be referenced from a static context」は、staticの中からインスタンスのメンバーを使ったとき。まずはnewしてインスタンス経由で使う直し方を検討する
  • static初期化は「初めて使われたとき」に1回、書いた順。コンパイル時定数の参照だけでは初期化されない
  • 外側の値を使わない入れ子のクラスはstaticネストクラスに。内部クラスは外側への参照(this$0)を持つ
  • 定数・ユーティリティ・ファクトリはstaticが向いている。可変の共有状態はスレッド競合とテストの順序依存を生むので、インスタンスやAtomicIntegerで扱う
  • Java 25ではstaticなしのvoid main()が正式機能(JEP 512)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java staticとは|static変数・メソッドの使い方とエラー例」

株式会社RJCでは、staticのような基本の仕組みを、実際に動かして確かめながら仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?