
Javaのstaticとは?初心者向けにstaticの仕組みや使い方を解説
Javaでフィールドにstaticと指定すると、クラスの持ち物であるstaticフィールド(static変数、クラス変数とも)となります。ご存じのとおり、フィールドとはローカル変数ではない変数のことです。
この記事ではstaticフィールドについて、宣言の仕方、使い方、活用例などについて初心者向けに解説します。
※この記事のサンプルは、Java 10の環境にて動作確認しています。
目次
1.staticフィールドとはクラスが持つフィールド
インスタンスフィールドとstaticフィールドの特徴は以下のとおりです。
- インスタンスフィールド:インスタンスを生成しないと使えない、何かのインスタンスに紐付いている、インスタンスごとに異なる変数
- staticフィールド:インスタンスを生成しなくても使える、クラスに紐付いている、クラスで一つの変数
つまり、フィールドがstaticかどうかの違いとは、そのフィールドが紐付いている先がインスタンスとクラスのどちらであるかです。そして、Javaではクラスはメモリ上に一つしかありませんので、staticフィールドもプログラム全体で一つしかないことが保証されるのです。
1-1.良くある勘違い:staticフィールドは必ずしも定数ではない
staticは「静的な、固定の」を意味する英単語なので、「staticフィールドは一回設定した値を変えられない(=定数の役割)」と思われる方がいるかもしれません。ですが、Javaでは変数の内容を不変とする修飾子はfinalという別のものなので、間違えないようにしましょう。
staticフィールドが静的・固定であるとは、Javaのメモリ上に専用の領域を静的・固定で確保しているということです。ですから、staticフィールドは一般のプログラミング言語で言うところの広域変数であり、定数(決めた値は未来永劫変えない、変わらない)では必ずしもないのです。この理解は重要です。
staticかつfinalなフィールドであれば、プログラム全体でいわゆる定数のように用いることはできます。これは普通に使いますが、finalでないstaticなフィールドをむやみやたらに使うと、過去のプログラミングで大問題となった広域変数の問題がJavaでも再発するので、大変危険です(詳細は後述します)。
2.staticフィールドの宣言の仕方
フィールドをstaticフィールドとするには、変数宣言において型の「前」に修飾子 static を付けるだけです。
class StaticFieldSample1 { int instanceField = 1; ← インスタンスフィールド static int staticField = 2; // ← staticフィールド /* int static staticField2 = 3; // コンパイルエラー!! staticを付ける場所が違うため */ }
変数名はインスタンスフィールド・staticフィールド間でも重複してはいけませんので、以下はコンパイルエラーとなります。
class StaticFieldSample2 { int someField = 1; static int someField = 2; // ← コンパイルエラー!! 1つのクラス内で変数名が重複しているため }
また、ローカル変数をstaticにはできませんので、以下もコンパイルエラーとなります。
class StaticFieldSample3 { void method() { static int x = 1; // コンパイルエラー!! ローカル変数はstaticとはできない } }
3.staticフィールドの使い方
インスタンスフィールドとstaticフィールドは何の持ち物かが違うので、使い方も以下のように違います。なお、同じクラスの中あるいはstatic importを行えば、staticフィールドを使う際の“クラス名.”は記述を省略できます。
- インスタンスフィールド:インスタンスを指す変数名.インスタンスフィールド名
- staticフィールド:クラス名.staticフィールド名
フィールドが属する先が違うだけなので、使い方そのものはインスタンスフィールドと同じです。
class StaticFieldSample4 { int instanceField = 1; // ← インスタンスフィールド static int staticField = 2; // ← staticフィールド void method() { System.out.println(staticField); // → 2、同じクラスの中からなのでstaticフィールドを直接参照できる } } class StaticFieldSample5 { public static void main(String[] args) { StaticFieldSample4 sample1 = new StaticFieldSample4(); // インスタンスを生成し、sample1という変数でインスタンスへの参照を確保 System.out.println(sample1.instanceField); // → 1 StaticFieldSample4 sample2 = new StaticFieldSample4(); // 別のインスタンスを生成し、sample2という変数でインスタンスへの参照を確保 sample2.instanceField = 100; // sample2が指すインスタンスのインスタンスフィールドの値を変更 System.out.println(sample1.instanceField); // → 1、sample1が指すインスタンスのインスタンスフィールドは変わらず1のまま System.out.println(sample2.instanceField); // → 100 System.out.println(StaticFieldSample4.instanceField); // コンパイルエラー!! インスタンスフィールドはインスタンスを生成しないと使えない System.out.println(StaticFieldSample4.staticField); // → 2、staticフィールドはクラス経由で直接参照できる } }
staticフィールドはクラスで一つだけ、というのは以下でも確認できます。どこかでstaticフィールドの値を変更すると、プログラム上でそのフィールドを参照している箇所全てで変更結果が反映されるのです。
class StaticFieldSample6 { static Object staticField = new Object(); void printStaticField() { System.out.println(staticField); // staticフィールドが指すものをインスタンスから確認 } } class StaticFieldSample7 { public static void main(String[] args) { // StaticFieldSample6のインスタンスを2つ作成 StaticFieldSample6 sample1 = new StaticFieldSample6(); StaticFieldSample6 sample2 = new StaticFieldSample6(); // staticフィールドが指すものを確認 sample1.printStaticField(); // → java.lang.Object@6b1d6fff sample2.printStaticField(); // → java.lang.Object@6b1d6fff System.out.println(StaticFieldSample6.staticField); // → java.lang.Object@6b1d6fff // staticフィールドが指すものを変更 StaticFieldSample6.staticField = new Object(); // staticフィールドが指すものを再確認 sample1.printStaticField(); // → java.lang.Object@299a06ac sample2.printStaticField(); // → java.lang.Object@299a06ac System.out.println(StaticFieldSample6.staticField); // → java.lang.Object@299a06ac } }
なお、staticフィールドもインスタンス経由で参照できます。しかし、staticフィールドだとプログラム上で分かりづらくなりますので、通常はクラス名から参照します。開発環境によっては警告が出ることすらあります。
class StaticFieldSample8 { static int staticField = 1; public static void main(String[] args) { System.out.println(StaticFieldSample8.staticField); // → 1 StaticFieldSample8 sample = new StaticFieldSample8(); System.out.println(sample.staticField); // → 1、インスタンス経由でもアクセスはできるが、普通はこう書かない } }
4.staticフィールドの初期化の仕方
staticフィールドの初期化の仕方は以下の2つがあります。
- 宣言時に値を設定する
- staticイニシャライザで値を設定する
なお、未初期化とした場合の初期値のルールはインスタンスフィールドと同じで、数値型プリミティブの場合は0、booleanの場合はfalse、参照型の場合はnullです。
class StaticFieldSample9 { static int staticIntField; static boolean staticBooleanField; static Object staticObjectField; public static void main(String[] args) { System.out.println(staticIntField); // → 0 System.out.println(staticBooleanField); // → false System.out.println(staticObjectField); // → null } }
4-1.宣言時に値を設定する
インスタンスフィールドやローカル変数と同様に、宣言時に値を設定できます。staticフィールドはクラスに一つですから、初期値が設定されるのはクラスの初期化と同タイミング、かつプログラム実行中に1回だけです。
class StaticFieldSample10 { static int staticIntField = 100; static boolean staticBooleanField = true; static Object staticObjectField = new Object(); public static void main(String[] args) { System.out.println(staticIntField); // → 100 System.out.println(staticBooleanField); // → true System.out.println(staticObjectField); // → Objectへの参照 } }
4-2.staticイニシャライザで値を設定する
インスタンスはコンストラクタ内でフィールドの初期化などを行えますが、クラスそのものにはコンストラクタがありません。
コンストラクタの代わりに、staticイニシャライザというクラスの初期化に使える構文がありますので、そこで値を設定できます。staticイニシャライザはブロックなので、長い処理も書けます。一方、宣言と同時の初期化では、一つの文で書ける内容でしか初期化できないのです。
staticイニシャライザとは、クラスの中に以下の書式で作成したブロックのことです。“static”の後ろにブロックを置き、ブロック中に必要な処理を書きます。finalなstaticフィールドで、宣言時に値を設定していない場合は、staticイニシャライザ内で初期化する必要があります。
static { // クラスの初期化時に実行したい処理 }
class StaticFieldSample11 { static List<String> staticListField; // Listのstaticフィールドを宣言 static final String staticStringField; // finalなstaticフィールドを宣言したが、値は未設定 static { // Listのインスタンスを生成してaddするには複数行が必要なので、staticイニシャライザ内で初期化する staticListField = new ArrayList<>(); staticListField.add("value1"); staticListField.add("value2"); staticListField.add("value3"); staticStringField = "String"; // staticイニシャライザ内で初期化を行わないとコンパイルエラー!! } }
staticイニシャライザではチェック例外をthrowできません。しかも、staticイニシャライザ内で発生した例外がcatchせずにthrowされるなら、そのクラスは永久に初期化が完了できず、プログラム中から使えません。適切にcatchするなどしなければなりません。
class StaticFieldSample12 { static { throw new Exception(); // コンパイルエラー!! staticイニシャライザ内ではチェック例外をthrowできない } }
class StaticFieldSample13 { static { // コンパイルエラーとはならないが、 // クラスの初期化中にRuntimeExceptionが必ずthrowされるので、 // 永久にクラスを使えない!! if (true) { throw new RuntimeException(); } } }
ちなみに、活用できる局面は少ないのですが、staticイニシャライザは一つのクラス中に複数記述できます。複数記述した場合は、記述した順番で(上から)実行されます。
class StaticFieldSample14 { // staticイニシャライザは① → ② → ③の順番で実行される static { // staticイニシャライザ① System.out.println(1); } static { // staticイニシャライザ② System.out.println(2); } static { // staticイニシャライザ③ System.out.println(3); } }
5.staticフィールドの活用例
5-1.定数として使う
最も一般的なstaticフィールドの使い方です。Javaでは定数はfinal修飾子で実現しますが、単にfinalなだけだと特定のインスタンスでしか使えない定数になり、インスタンスを生成しないと使えないなど使い勝手が悪いので、一般的にはstaticと組み合わせます。
class StaticFieldSample15 { public static final double PI = 3.14; // 円周率 } class StaticFieldSample16 { double caluarteCircleArea(double radius) { return radius * radius * StaticFieldSample15.PI; // 面積 = 半径 x 半径 x 円周率(定数) } }
staticかつfinalなフィールドを定数として使うことは実務でも一般的に行います。ですが、お互いに関係のない定数がずらりと並んだ、いわゆる定数クラスや定数インターフェイスは「Javaらしい」ものではありません。クラスの存在意義である分割統治や適切な役割分担に真っ向から反する使い方だからです。あくまで必要悪です。それに、定数を使うのであれば、今ならenumも積極的に活用しましょう。
// プログラムで使う定数を一つのクラス/インターフェイスで管理するのは、今ではバッドノウハウ public class Constant { public static final int SUCCESS = 1; public static final int FAIL = 2; public static final String JDBC_URL = "jdbc:..."; public static final String JDBC_PASSWORD = "abcdefg"; … }
また、以下のとおり参照型変数をstatic finalとしても、変数が指している先のインスタンスが保持している値が固定されることとはイコールではありませんので、こちらも意識する必要があります。
class StaticFieldSample17 { public static final int[] CONSTANT_ARRAY = {1, 2 ,3}; } class StaticFieldSample18 { public static void main(String[] args) { StaticFieldSample17.CONSTANT_ARRAY[1] = 123; // ← 配列の要素の書き換えはできてしまう } }
5-2.ロック(排他制御)用の変数として使う
クラスから生成された全インスタンスでの排他制御を行う際のロック用オブジェクトとして、staticフィールドを使うことがあります。
class StaticFieldSample19 { private static final Object LOCK = new Object(); void method() { // クラスで唯一のロックオブジェクトをモニターとして処理を同期させる synchronized (LOCK) { // クラスの全インスタンスで同期させて実行させたい処理 } } }
staticと指定しなかった場合は同一インスタンス内での同期実行が保証されますが、別のインスタンスも含めた同期は行えません。そのためには、インスタンスを超えたところにロック用のオブジェクトが必要で、そのためにstaticフィールドを使います。
今では排他制御にはjava.util.cuncurrent.locksのクラスを使うことが普通ですが、その場合でもクラス単位での排他制御が必要なら、staticフィールドを用います。
import java.util.concurrent.locks.ReentrantLock; class StaticFieldSample20 { private static final ReentrantLock lock = new ReentrantLock(); // ReentrantLockをstaticフィールドで生成 void method() { lock.lock(); // ReentrantLockで排他制御を実行 try { // 全てのインスタンスで同期させて実行させたい処理 } finally { lock.unlock(); // ロックは必ず解除する } } }
5-3.1つだけのインスタンスを保証する(Singletonパターン)
あるクラスのインスタンスが1つだけしかないことを保証したいケースがあります。その場合に良く使うのはデザインパターンのSingletonですが、これはstaticフィールドの活用事例としても有名なものです。
class StaticFieldSample21 { // 自分自身の唯一のインスタンスを持つstaticフィールド private static StaticFieldSample21 instance; // コンストラクタをprivateとして、外部からインスタンスを生成させない private StaticFieldSample21() { } // 唯一のインスタンスが未初期化ならnewし、そうでなければstaticフィールドの値をreturn // ※マルチスレッドを考慮するなら、synchronizedメソッドとすべき static StaticFieldSample21 getInstance() { if (instance == null) { instance = new StaticFieldSample21(); } return instance; } }
staticフィールドに自分のクラスのインスタンスを持ち、さらにコンストラクタはprivateとして外部からのインスタンス生成を不可能とします。インスタンスを取得するためのstaticメソッドを作り、その中だけでインスタンスを生成すれば、Singletonパターンの出来上がりです。
Singletonに限らず、フィールドに自分自身のクラスを持つ、というのが初心者には分かりづらいかもしれませんが、こういう書き方もOKなのです。
5-4.固定的なインスタンスのキャッシュに使う
Javaは必要に応じていくらでもインスタンスをnewできるのがいいところですが、それでもインスタンスの生成を制限したり、インスタンスを使いまわしたいケースがあります。その場合にもstaticフィールドが活用できます。
例えば、Integerはインスタンスを使いまわします。Integerはintのラッパークラスで、何かの整数を表現するクラスです。Integer自体は不変(Immutable)なので、同じ数字(例えば1)なのに違うインスタンスを都度生成してはメモリの無駄です。ですので、staticフィールドでインスタンスを使いまわしています。
以下はJavaのInteger.valueOf(int)のソースコードです。IntegerCacheというクラスのstaticフィールドはIntegerの配列で、valueOfの引数で指定された整数が配列の範囲内であれば、そこのインデックスにあるIntegerのインスタンスを戻しています。newするのは配列の範囲外の整数の場合だけです。
// java.lang.Integerのソースコードの抜粋(Java 10版) public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; …省略… }
実際にvalueOfで戻るIntegerが同じインスタンスか確認してみましょう。小さな整数(0や1とか)はプログラムでは良く使われるので、キャッシュを利用してインスタンス生成を効率化しているのですね。この考えは、デザインパターンのFlyweightパターンにも通じるものがあります。
class StaticFieldSample22 { public static void main(Sting[] args) { Object i1 = Integer.valueOf(1); Object i2 = Integer.valueOf(1); Object i3 = Integer.valueOf(65535); Object i4 = Integer.valueOf(65535); System.out.println(i1 == i2); // → true 同じインスタンス!! System.out.println(i3 == i4); // → false 違うインスタンス!! } }
6.staticフィールドの悪用例
staticフィールドはいわゆるグローバル変数的には使うべきではありません。例えば以下のような感じです。
class StaticFieldSample23 { static int input; // 入力パラメータとして使うstaticフィールド static int output; // 出力パラメータとして使うstaticフィールド // 何かの計算を行うメソッド static void calculate() { output = input * 100; // 計算はstaticフィールドで行い、結果もstaticフィールドで戻す } } class StaticFieldSample24 { public static void main(String[] args) { StaticFieldSample23.input = 123; StaticFieldSample23.calculate(); System.out.println(StaticFieldSample23.output); // → 123 * 100の結果 } }
class StaticFieldSample25 { static int sharedVariable; // 複数のクラス間で共有したい値 } class StaticFieldSample26 { void setSharedVariable() { StaticFieldSample25.sharedVariable = 100; // こちらのクラスで設定して… } } class StaticFieldSample27 { void getSharedVariable() { System.out.println(StaticFieldSample25.sharedVariable); // こちらのクラスで読みだす } }
なぜstaticフィールドをこのように使ってはならないのかというと、これは昔からあるグローバル変数(広域変数)の使い方そのものであるからです。グローバル変数は手軽に使えて便利な反面、以下のデメリットがあり、数多くのバグを生み出してきました。
- どこでそのグローバル変数が使われているか把握するのが難しい
- 誰がどこでグローバル変数の値を変更したかが簡単には追いかけられない
- グローバル変数の設定値が正しいと保証するのが難しい(マルチスレッド環境だと顕著)
そもそもJavaなどのオブジェクト指向プログラミング言語は、このような課題を改善するために生まれてきました。値をグローバル変数ではなくクラスに持たせて変数を管理する主体を明確にし、かつ変数のカプセル化によりそれを徹底するのが、オブジェクト指向プログラミング言語での考え方です。
staticフィールドをグローバル変数的に使えるからと言って、過去のプログラミング言語のような使い方をしていては、なくせるバグもなくせません。せっかくJavaを使うのですから、バグが少なくなるプログラムの作り方をしたいものですね。
7.まとめ
staticフィールドとは、クラスの持ち物であるフィールドです。使い方そのものはインスタンスフィールドと似ていますが、クラス唯一の変数であることは常に念頭に置いておきましょう。
staticフィールドの使い方・活用方法は様々で、便利に使えます。ですが、むやみやたらにグローバル変数的に用いることはプログラム言語の進化の意味に反することでもあるので、どのように使うかを良く考えましょう。
コメント