The JLS describes a few special behaviors of what it calls constant variables, which are final
variables (whether static
or not) which are initialized with constant expressions of String
or primitive type.
Constant variables have a major difference with respect to binary compatibility: the values of constant variables become part of the class's API, as far as the compiler is concerned.
An example:
class X {
public static final String XFOO = "xfoo";
}
class Y {
public static final String YFOO;
static { YFOO = "yfoo"; }
}
class Z {
public static void main(String[] args) {
System.out.println(X.XFOO);
System.out.println(Y.YFOO);
}
}
Here, XFOO
is a "constant variable" and YFOO
is not, but they are otherwise equivalent. Class Z
prints out each of them. Compile those classes, then disassemble them with javap -v X Y Z
, and here is the output:
Class X:
Constant pool:
#1 = Methodref #3.#11 // java/lang/Object."<init>":()V
#2 = Class #12 // X
#3 = Class #13 // java/lang/Object
#4 = Utf8 XFOO
#5 = Utf8 Ljava/lang/String;
#6 = Utf8 ConstantValue
#7 = String #14 // xfoo
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = NameAndType #8:#9 // "<init>":()V
#12 = Utf8 X
#13 = Utf8 java/lang/Object
#14 = Utf8 xfoo
{
public static final java.lang.String XFOO;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String xfoo
X();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
}
Class Y:
Constant pool:
#1 = Methodref #5.#12 // java/lang/Object."<init>":()V
#2 = String #13 // yfoo
#3 = Fieldref #4.#14 // Y.YFOO:Ljava/lang/String;
#4 = Class #15 // Y
#5 = Class #16 // java/lang/Object
#6 = Utf8 YFOO
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 <clinit>
#12 = NameAndType #8:#9 // "<init>":()V
#13 = Utf8 yfoo
#14 = NameAndType #6:#7 // YFOO:Ljava/lang/String;
#15 = Utf8 Y
#16 = Utf8 java/lang/Object
{
public static final java.lang.String YFOO;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Y();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #2 // String yfoo
2: putstatic #3 // Field YFOO:Ljava/lang/String;
5: return
}
Class Z:
Constant pool:
#1 = Methodref #8.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #17 // X
#4 = String #18 // xfoo
#5 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Fieldref #21.#22 // Y.YFOO:Ljava/lang/String;
#7 = Class #23 // Z
#8 = Class #24 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = NameAndType #9:#10 // "<init>":()V
#15 = Class #25 // java/lang/System
#16 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#17 = Utf8 X
#18 = Utf8 xfoo
#19 = Class #28 // java/io/PrintStream
#20 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#21 = Class #31 // Y
#22 = NameAndType #32:#33 // YFOO:Ljava/lang/String;
#23 = Utf8 Z
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
#31 = Utf8 Y
#32 = Utf8 YFOO
#33 = Utf8 Ljava/lang/String;
{
Z();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String xfoo
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: getstatic #6 // Field Y.YFOO:Ljava/lang/String;
14: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: return
}
Things to notice in the disassembly, which tell you the differences between X
and Y
run deeper than syntactic sugar:
XFOO
has a ConstantValue
attribute, signifying that its value is a compile-time constant. Whereas YFOO
does not, and uses a static
block with a putstatic
instruction to initialize the value at runtime.
The String
constant "xfoo"
has become part of class Z
's constant pool, but "yfoo"
has not.
Z.main
uses the ldc
(load constant) instruction to load "xfoo"
onto the stack directly from its own constant pool, but it uses a getstatic
instruction to load the value of Y.YFOO
.
Other differences you will find:
If you change the value of XFOO
and recompile X.java
but not Z.java
, you have a problem: class Z
is still using the old value. If you change the value of YFOO
and recompile Y.java
, class Z
uses the new value whether you recompile Z.java
or not.
If you delete the X.class
file entirely, class Z
still runs correctly. Z
has no runtime dependency on X
. Whereas if you delete the Y.class
file, class Z
fails to initialize with a ClassNotFoundException: Y
.
If you generate documentation for the classes with javadoc, the "Constant Field Values" page will document the value of XFOO
, but not the value of YFOO
.
The JLS describes the above effects constant variables have on compiled class files in §13.1.3:
A reference to a field that is a constant variable (§4.12.4) must be resolved at compile time to the value V denoted by the constant variable's initializer.
If such a field is static
, then no reference to the field should be present in the code in a binary file, including the class or interface which declared the field. Such a field must always appear to have been initialized (§12.4.2); the default initial value for the field (if different than V) must never be observed.
If such a field is non-static
, then no reference to the field should be present in the code in a binary file, except in the class containing the field. (It will be a class rather than an interface, since an interface has only static
fields.) The class should have code to set the field's value to V during instance creation (§12.5).
And in §13.4.9:
If a field is a constant variable (§4.12.4), and moreover is static
, then deleting the keyword final
or changing its value will not break compatibility with pre-existing binaries by causing them not to run, but they will not see any new value for a usage of the field unless they are recompiled.
[...]
The best way to avoid problems with "inconstant constants" in widely-distributed code is to use static
constant variables only for values which truly are unlikely ever to change. Other than for true mathematical constants, we recommend that source code make very sparing use of static
constant variables.
The upshot is that if your public library exposes any constant variables, you must never change their values if your new library version is otherwise supposed to be compatible with code compiled against old versions of the library. It won't necessarily cause an error, but the existing code will probably malfunction since it will have outdated ideas about the values of constants. (If your new library version needs for classes which use it to be recompiled anyway, then changing constants doesn't cause this problem.)
Thus, initializing a constant with a block gives you more freedom to change its value, because it prevents the compiler embedding the value into other classes.