概要
Java では「static final 変数を定数と呼ぶのが慣習」だと思っていたが、調べてみたところ全然違った。
Java の定数は「宣言時に初期化された、final 修飾子のある プリミティブ型 または String 型の変数」で、コンパイル時に値が確定しリテラルと同値となる性質をもつ。
本稿は Java 11 による実行結果を基にし、ドキュメントも可能な限り同バージョンのものを参照する。
定数の定義
Java における定数は The Java Language Specification 4.12.4 final Variables に定義されている。
A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.28).
この定義は 3 つの条件を含んでいる。
-
final variable
: final 修飾子のついた変数であること -
primitive type or type String
: プリミティブ型または String 型であること -
initialized with a constant expression
: constant expression で初期化されていること
constant expression とは、プリミティブ型 または String 型のリテラルと、それらの演算で構成された式のこと。
これらの条件を満たす値はコンパイル時に確定し1、class ファイルのコンスタントプール2に格納される。
どのスコープに定義してもリテラルと同値
定義から分かる通り、定数は static フィールドである必要はない。どのスコープに定義しても、定数の条件を満たす変数はコンパイル時にリテラルと同値になる。
次のコードを例に static フィールドと文字列リテラルを比較する。
...
private static final String CONSTANT = "CONSTANT";
public void method() {
System.out.println(CONSTANT);
System.out.println("literal");
}
...
コンパイルして得られた class ファイルを javap -v
で逆アセンブルすると、以下の出力が得られる。
Constant pool:
...
#4 = String #8 // CONSTANT
#5 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = String #24 // literal
#7 = Class #25 // java/lang/Object
#8 = Utf8 CONSTANT
...
#24 = Utf8 literal
...
public void method();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String CONSTANT
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #6 // String literal
13: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
...
メソッド method
内において、static フィールド CONSTANT
も文字列リテラル literal
も、コンスタントプールを参照していることがわかる。
3: ldc #4
はコンスタントプールの #4
を参照してスタックに PUSH する。コンスタントプール #4
は同 #8
の参照であり、ここに文字列 CONSTANT
が格納されている。文字列リテラルを参照する 11: ldc #6
も、同じ命令で文字列 literal
を参照している。
定数の演算は定数になる
長い文字列を表現するために、複数の文字列を +
演算子で結合することがある。式が定数とリテラルで構成されていれば、結合された文字列が定数としてコンスタントプールに格納される。
すなわち、前述のコードと次のコードのコンパイル結果は同じになる。
...
private static final String CONSTANT = "CONST" + "ANT";
public void method() {
System.out.println(CONSTANT);
System.out.println("liter" + "al");
}
...
なお、式で参照する変数に final 修飾子がついていないと、演算結果は定数にならない。参照された変数が定数でないため。
// System.out.println("hello world") と等価
final String hello = "hello";
System.out.println(hello + " world");
// 結合後の文字列はコンパイル時に確定しない
String hello = "hello";
System.out.println(hello + " world");
数値などプリミティブ型の式も同様に、演算結果がコンスタントプールに格納される。
いずれも式は constant expression の制約を満たす必要がある。とはいえ、使用できない演算子は明示されている限り ++
--
instanceof
のみで、ほとんど気にする必要はないだろう。
定数を含むオブジェクトの演算について、より多くのバリエーションは Stringとheap領域とconstant pool - Qiita が詳しい。
定数のメモリ消費
static 変数はヒープ領域に読み込まれ、通常はアプリケーションの終了まで破棄されない3。これを踏まえると、局所的なリテラル文字を使用した方がメモリを節約できそうに思える。
しかし先に見たように、どちらも同じコンパイル結果になることから、メモリ消費の観点でも両者に違いはないとみて良いだろう。
ところで、定数はどのメモリ領域に格納されるのだろうか?
The Java Virtual Machine Specification 5.1. The Run-Time Constant Pool によれば、コンスタントプールにある文字列が以前に読み込まれていなければ、新しい String インスタンスを生成するようである。また、Java 8 において、従来ヒープ領域に存在した Permanent Generation を削除し、ネイティブメモリ領域を利用する Metaspace を追加した JEP122 の記述によれば、static 変数および文字列の正準表現はヒープ領域に格納するとされている4。したがって、文字列定数を含むクラスがロードされた時点で、コンスタントプールにある文字列(の少なくとも正準表現)はヒープ領域に格納される。
一方、実行時コンスタントプールは Method Area というメモリ上の概念領域に割り当てられる。OpenJDK の JVM 実装である HotSpot において、その実態は Metaspace のようだ5。
わからなかったこと
コンパイル時に確定できない値を、実行時に定数のように扱う Condy という仕組みが Java 11 で導入されたそうだが、逆アセンブル結果に BootstrapMethods セクションが出力される検証コードを作ることができなかった。備忘のためにリンクしておく。
- Java 11 / JEP 309のConstant Dynamic(condy)を試す - Fight the Future
- The Java Virtual Machine Specification - 5.4.3.6. Dynamically-Computed Constant and Call Site Resolution に相当すると思われる
参考
- Java performance : private static final String vs local String ? number of object created in Heap Space - Stack Overflow
- Understanding the constant pool inside a Java class file
-
Javaにおける定数 - きどたかのブログ によれば、過去の仕様では
compile-time constant expression
と表現されていたらしい。 ↩ -
The Java Virtual Machine Specification では
constant table
と呼んでいるようだが、本稿では逆アセンブル結果に合わせてこれをコンスタントプールと呼び、メモリ上にロードされたコンスタントプールを特に実行時コンスタントプール (runtime constant pool) と呼ぶ。 ↩ -
厳密には、そのクラスをロードしたクラスローダーが破棄されるまで。The Java Language Specification - 12.7. Unloading of Classes and Interfaces ↩
-
The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap.
↩ -
jvm - Is Method area still present in Java 8? - Stack Overflow ↩