The answer is fairly simple: As a shortcut, java treats certain concepts as a so-called 'compile time constant' (CTC). The idea is to entirely inline a variable, at the compilation level (which is extraordinary; normally javac
basically just bashes your java source file into a class file using very simple and easily understood transformations, and the fancypants optimizations occur at runtime during hotspot).
For example, if you do this:
Save to UserOfBatch.java
:
class BatchOConstants {
public static final int HELLO = 5;
}
public class UserOfBatch {
public static void main(String[] args) {
System.out.println(BatchOConstants.HELLO);
}
}
Run on the command line:
> javac UserOfBatch.java
> java UserOfBatch
5
> javap -c UserOfBatch # javap prints bytecode
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_5
4: invokevirtual #15 // Method java/io/PrintStream.println:(I)V
7: return
Check out line 3 up there. iconst_5
. That 5? It was hardcoded!!
No reference to BatchOConstants
remains. Let's test that:
on command line:
> rm BatchOConstants.class
> java UserOfBatch
5
Wow. The code ran even though it's missing the very class file that should be providing that 5, thus proving, it was 'hardcoded' by the compiler itself and not the runtime.
Another way to 'observe' CTC-ness of any value is annoparams. An annotation parameter must be hardcoded into the class file by javac, so you can't pass expressions. Given:
public @interface Foo {
long value();
I can't write: @Foo(System.currentTimeMillis())
, because System.cTM
obviously isn't a compile time constant. But I can write @Foo(SomeClass.SOME_STATIC_FINAL_LONG_FIELD)
assuming that the value assigned to S_S_F_L_F is a compile time constant. If it's not, that @Foo(...)
code would not compile. If it is, it will compile: CTC-ness now determines whether your code compiles or not.
There are specific rules about when the compiler is allowed to construe something as a 'compile time constant' and go on an inlining spree. For example, null
is not an inline constant, ever. Oversimplifying, but:
- For fields, the field must be static and final and have a primitive or String type, initialized on the spot (in the same breath, not later in a static block), with a constant expression, that isn't null.
- For local variables, the rules are very similar, except, the need for them to be static is obviously waived as they cannot be. Other than that, all the fixins apply: final, primitive-or-String, non-null, initialized on the spot, and with a constant expression.
Unfortunately, CTC-ness of a local is harder to directly observe. The code you wrote is a fine way to indirectly observe it though. You've proven with your prints that firstName
is CTC and lastName
is not.
We can observe a select few things though. So let's take your code, compile it, and toss it at javap to witness the results. Via Jonas Konrad's online javap tool, let's analyse:
public Main() {
final String a = "hello";
String b = "world";
String c = a + "!";
String d = b + "!";
System.out.println(c == "hello!");
System.out.println(d == "world!");
}
The relevant parts:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: ldc #7 // String hello
6: astore_1
start local 1 // java.lang.String a
7: ldc #9 // String world
9: astore_2
start local 2 // java.lang.String b
10: ldc #11 // String hello!
12: astore_3
start local 3 // java.lang.String c
13: aload_2
14: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
19: astore 4
start local 4 // java.lang.String d
Note how 'start local 2' (which is c; javap starts counting at 0) shows just loading hello!
as a complete constant, but 'start local 3' (which is d) shows loading 2 constants and invoking makeConcat to tie em together.