Bits of Java (トップ)

VM   JDK と JRE
Language   オブジェクトとインスタンス   this と super   static   フィールドアクセス
IO   Serializable の実装
Swing   MetalLookAndFeel   イメージパネル
JavaBeans   プロパティ名について   XMLEncoder で保存
その他   正規表現テストアプリ   秀丸の強調表示   Ant のインストール   文字セット変換 Ant タスク   XAMPP + Tomcat

static について

キーワード static はフィールドやメソッド、ネストされたクラスを宣言する際の修飾や static ブロック (静的初期化子) を記述する際に使用されます。

static 指定して宣言されているフィールドは "static フィールド"、 "static 変数" あるいは "クラス変数" などと呼ばれます。 static が指定されていないフィールドはインスタンス変数です。 以下はインスタンス変数とクラス変数の違いに関するプログラムです。

public class Foo {
    public static int value;
    public String text;
}

public class Main {
    public static void main(String[] args) {
        Foo.value = 100;
        System.out.println(Foo.value);
        Foo foo = new Foo();
        foo.value = 1000;
        Foo foo2 = new Foo();
        System.out.println(foo2.value);
    }
}
Main の実行結果は
100
1000
が標準出力に表示されます。

まず main メソッドの最初の部分でクラス Foo が必要になります。

        Foo.value = 100;
        System.out.println(Foo.value);
少なくとも Foo.value にアクセスする段階では JVM によってクラス Foo のロードが済んでいます。 この Foo のロードによって JVM が使用しているメモリのヒープと呼ばれる領域内の専用エリアに Foo の内部表現の展開と Foo を表現する Class オブジェクトの生成が行われます。 このクラス表現を展開する為のヒープ内の専用領域は以前はメソッドエリアと呼ばれていましたが Hotspot VM においてはヒープの Permanent Generation 領域と言うそうです。 この専用領域に展開されるクラスの内部表現とは通常は読み込んだクラスファイルをもとに JVM の実装に依存する形式で作成され、バイトコードを含むメソッド情報やフィールド情報、クラス内で使用される定数などと共にクラス変数も含んでいます。 つまり Foo のクラス変数 value はロードによって作成された Foo の内部表現の一部として存在します。 このようにクラス変数はそれが定義されているクラスがロードされた際に作成されるので、ロードされたクラスごとに1つずつしか JVM 上に存在しません。

クラス変数はクラスのロード時に作成されているのでインスタンスを生成する必要はなく

クラス名 + . (ドット) + 変数
でアクセスします。 Foo のロード後は Foo.value でその値を取得したり、代入の際の左辺に Foo.value を指定すれば右辺の式の結果がメモリの Foo の内部表現の中の value の値として格納されます。 上のコードでは value の値を 100 に設定してからその値を標準出力に表示しています。
        Foo foo = new Foo();
次に Foo のインスタンスを生成しています。 インスタンス (オブジェクト) はヒープのインスタンスを置くための領域に生成されます。 インスタンスは new などで生成する際にその雛形となったクラスとそのクラスを Object クラスまでたどったすべての上位クラスに定義されているインスタンス変数を private 指定されているものを含め全部保持しています。 Foo の場合にはインスタンス変数 text は Foo のインスタンスが生成されるごとにそれぞれのインスタンスの内部に作成されることになります。 インスタンス変数とクラス変数の一番の違いはクラス変数は JVM 上に1つしかなく、インスタンス変数はインスタンスごとに生成される事と言えると思います。
        foo.value = 1000;
        Foo foo2 = new Foo();
        System.out.println(foo2.value);
Foo のインスタンスを生成後にその Foo オブジェクトの参照を通してクラス変数にアクセスしています。 このようにオブジェクトの参照を使ってクラス変数にアクセスする事は推奨されていません。 (Java言語コーディング規約  日本語訳)
クラス名を使ってアクセスする場合には一目でクラス変数である事が分かりますが、オブジェクトの参照を使ったアクセスではインスタンス変数と区別がつきません。 上位クラスのクラス変数にアクセスするつもりがそのクラス変数を隠蔽している同じ名前の別の変数にアクセスする可能性もあります。 クラス変数や static メソッドにアクセスする場合にはそれを宣言しているクラスまたはインターフェイスの型名を使ってアクセスするべきです。 (自身のクラスで宣言している場合は別です。また上位クラスから継承しているクラス変数や static メソッドの場合も単純名でアクセスしている場合が多いようです。)

上の例の場合にはローカル変数 foo と foo2 がそれぞれ別の Foo オブジェクトの参照を保持しており、それぞれのオブジェクトを通して value にアクセスしていますが、結局 JVM 上に1つしかない Foo の内部表現の value にアクセスする事になるので、どのオブジェクトを通してもアクセス対象は同じになります。


static メソッドは宣言時に static 指定されたメソッドでクラスメソッドとも言います。 宣言時に static 指定されていないメソッドはインスタンスメソッドです。 インスタンスメソッドの実行時には JVM 内部ではそのインスタンスメソッドが呼び出されたオブジェクトの参照が実行オブジェクトとして引数と共に渡されおり、ソースコードにおけるメソッド内の this や super が表す参照はこの実行オブジェクトになります。 また単純名によるインスタンスメソッドやインスタンス変数へのアクセスは実行オブジェクトや実行オブジェクトのエンクロージングオブジェクトに対するアクセスとなるので this や クラス名.this などでオブジェクトを明示する事が可能です。

static メソッドはオブジェクトに属すものではないので実行オブジェクトはありません。 よって static メソッド実行時に引数と共に実行オブジェクトの参照が渡される事もありません。 実行オブジェクトが存在しないので static メソッド内で this や super による実行オブジェクト (またはそのエンクロージングオブジェクト) へのアクセスは不可能で、また単純名によるインスタンス変数やインスタンスメソッドへのアクセスも対象となる実行オブジェクトが存在しない為できません。 もしそのようなコードがある場合にはコンパイルエラーになります。 また実行オブジェクトが無いので static メソッドを呼び出すコードは通常

    クラス名.メソッド名
と記述します。
    オブジェクトの参照.メソッド名
のようにオブジェクトを通してアクセスしているコードもコンパイル時のチェックで static メソッドへのアクセスと判断されると invokestatic という static メソッドを呼び出すバイトコードが生成され、 実行時のメソッド呼び出しにおいてオブジェクトの参照が引数と共に渡される事はありません。

static メソッドにおいてはクラス変数 (static 変数) へのアクセスは問題ありません。 クラス変数はクラスのロードによってメモリ上に生成されたクラスの内部表現の一部として存在するのでアクセスするのに実行オブジェクトは必要無いからです。 また static メソッドから別の static メソッドを呼び出す事も実行オブジェクトは必要ないので問題ありません。

注意が必要なのは static メソッド内ではインスタンス変数やインスタンスメソッドへアクセスができないのではなく、実行オブジェクトが無いので実行オブジェクトへのアクセスができないという点です。

public class Foo {
    private final long timestamp;
    private Foo() {
        timestamp = System.currentTimeMillis();
    }

    public long getTimestamp() {
        return timestamp;
    }

    public static Foo getInstance() {
        Foo foo = new Foo();
        System.out.println(foo.getTimestamp());
        return foo;
    }
}
static な getInstance メソッドでは Foo のインスタンスメソッドである getTimestamp を呼び出していますがこれは実行オブジェクトにアクセスしようとしているのではないので問題ありません。


static ブロック (静的初期化子) はクラス変数初期化子と共にロードされたクラスの初期化の際に実行されます。 インスタンス初期化子やインスタンス変数初期化子はコンストラクタを実行しているオブジェクトが実行オブジェクトとして存在しますが、 static ブロックやクラス変数初期化子は static メソッドと同様に実行オブジェクトはありません。 よって static メソッドと同様に static ブロックやクラス変数初期化子でも this や super による実行オブジェクト (またはそのエンクロージングオブジェクト) へのアクセスは不可で、また単純名によるインスタンス変数やインスタンスメソッドへのアクセスも対象となる実行オブジェクトが存在しない為できません。そのようなコードがある場合にはコンパイルエラーになります。 また static ブロックから例外をスローするような処理も不可で、そのようなコードもコンパイルエラーとなります。もしコンパイル時にチェックできない非検査例外などが実行時にクラス変数初期化子や static ブロックの処理中にスローされた場合には java.lang.ExceptionInInitializerError が発生します。

下は static ブロックやクラス変数初期化子の実行に関するプログラムです。

public class Sub extends Super {
    static int value = show(getCOMPILE_TIME_CONST_1());

    static {
        //System.out.println("value2 : " + value2);
        System.out.println("value2 : " + getValue2());
        value  = show(2);
        value2 = show(3);
    }
    static int value2 = show(4);
    static final int COMPILE_TIME_CONST_1 = 1;

    public static int show(int value) {
        System.out.println("called show : " + value);
        return value;
    }

    private static int getValue2() {
        return value2;
    }

    private static int getCOMPILE_TIME_CONST_1() {
        return COMPILE_TIME_CONST_1;
    }
}

public class Super {
    static {
        System.out.println("Super#static-initializer");
    }
}

public class Main {
    public static void main(String[] args) {
        Sub.show(100);
    }
}

Main の実行結果は以下が標準出力に表示されます。

Super#static-initializer
called show : 1
value2 : 0
called show : 2
called show : 3
called show : 4
called show : 100
順を追ってみていきます。
    public static void main(String[] args) {
        Sub.show(100);
    }
main メソッドでは Sub クラスの static メソッドの show を呼び出しています。 show メソッドの実行の前にはクラスの初期化が済んでいなければなりません ( Java言語規定 第3版 12.4.1 When Initialization Occurs )。 クラスの初期化とはクラス変数初期化子と static ブロックをソースコードにおける出現順 (上から順番) に処理することです。 クラスの初期化を行う際には先にスーパークラスの初期化が済んでいる必要があります。 よって Sub の初期化の前にはスーパークラスの Super の初期化が行われます。
    static {
        System.out.println("Super#static-initializer");
    }
Super クラスの static ブロックが実行され標準出力に
Super#static-initializer
が表示されます。 次に Sub クラスの初期化が行われます。 Sub の場合には最初に
    static int value = show(getCOMPILE_TIME_CONST_1());
が宣言されています。 初期化はソースコードにおける出現順 (上から順番) に行われるのが基本ですが、例外があり static final なフィールドで宣言時に 定数式 で初期化されているフィールドがある場合にはそれらが先に初期化されます。 よって Sub では COMPILE_TIME_CONST_1 が最初に 1 に初期化されます。
    show(getCOMPILE_TIME_CONST_1())  は  show(1)
となり
called show : 1
が表示されクラス変数 value に 1 が設定されます。
    static {
        //System.out.println("value2 : " + value2);
        System.out.println("value2 : " + getValue2());
        value  = show(2);
        value2 = show(3);
    }
次に static ブロックが実行されます。 最初は static メソッドの getValue2 で value2 の値を取得して標準出力に表示しています。 コンストラクタ実行の前にインスタンス変数がデフォルトの値に設定されているようにクラスの初期化に先立ってクラス変数はデフォルトの値に設定されています。 デフォルトの値とは boolean 型は false 、char 型は '' (空文字 '\u0000') 、それ以外のプリミティブ型は 0 、参照型は null です。 よって getValue2 メソッドは value2 に設定されているデフォルトの値の 0 を返し、標準出力に
value2 : 0
が表示されます。
もし、この static ブロックに
        System.out.println("value2 : " + value2);
というコードがある場合にはコンパイルエラーになります。 後方で宣言されたクラス変数の値を取得することはクラス変数初期化子や static ブロック (静的初期化子) 内ではできません。 ただし static ブロック内で代入の左辺としてなら後方で宣言されたクラス変数でも使用できます。 この規定は直接クラス変数の値を取得しようとしている場合をコンパイラがチェックするもので、 getValue2 メソッドのように static メソッド内でのクラス変数の値の取得が後方参照になるかどうかまではチェックできません。 このような初期化中のフィールド使用の制限はインスタンス変数とインスタンス変数初期化子、インスタンス初期化子、インスタンスメソッドにおいても static の場合と同様の関係が成り立ちます。

続いて static ブロックの残りの部分が処理されます。

        value  = show(2);
        value2 = show(3);
この呼び出しにより標準出力に
called show : 2
called show : 3
が表示され、 value と value2 にそれぞれ 2 と 3 が設定されます。
    static int value2 = show(4);
最後に上のクラス変数初期化子が実行されて
called show : 4
が表示され、value2 には 4 が設定されます。 これで Sub の初期化が終了します。
        Sub.show(100);
Sub クラスの初期化が済んでから上の main メソッドのコードが実行され、標準出力に
called show : 100
が表示されます。


ネストされたクラスが static 指定されている場合にはそのクラスのインスタンスはエンクロージングオブジェクトを持たない事を宣言している事になります。 よって外側のクラスのインスタンス変数やインスタンスメソッドに単純名や クラス名.this や クラス名.super でアクセスするコードがあれば、エンクロージングオブジェクトへのアクセスになるのでコンパイルエラーとなります。

下は java.awt.geom.Rectangle2D のソースの一部です。

package java.awt.geom;

public abstract class Rectangle2D extends RectangularShape {

    public static class Float extends Rectangle2D {
        public Float() {}
        public Float(float x, float y, float w, float h) {
            setRect(x, y, w, h);
        }
    }

    public static class Double extends Rectangle2D {
        public Double() {}
        public Double(double x, double y, double w, double h) {
            setRect(x, y, w, h);
        }
    }

}
抽象クラスの Rectangle2D クラスのコンクリートクラスとして Rectangle2D.Float と Rectangle2D.Double の2つのクラスがネストして定義されています。 この場合のインスタンスの生成は
    Rectangle2D.Float rectF = new Rectangle2D.Float(0, 0, 10.5f, 10.5f);
のようになります。

インスタンスメソッド、コンストラクタ、インスタンス初期化子内のローカルクラスや匿名クラス、あるいはインスタンス変数初期化子における匿名クラスのインスタンスはインスタンスメソッドやコンストラクタを実行しているオブジェクトをエンクロージングオブジェクトとして内部的に保持してますが、 static ブロックや static メソッド、クラス変数初期化子の実行の際には実行オブジェクトは存在せず、それらにおけるローカルクラスや匿名クラスのインスタンスはエンクロージングオブジェクトを持ちません。

public class Foo {
  int value = 100;

    static Object runnable = new Runnable() {
        public void run() {}
    };

    static {
        class Bar{}
    }

    public static void test() {
        class Hoge {}
    }
}
上の Foo にネストされている Runnable を実装した匿名クラスや Bar や Hoge のインスタンスはエンクロージングオブジェクトを持ちません。 もしこれらのクラスに単純名で value にアクセスするコードがある場合にはエンクロージングオブジェクトへのアクセスとしてコンパイル時にエラーが発生します。


最後に static と非 static に関して箇条書きにします。