Variable Modification with final
Applying final to primitives creates an immutable value that cannot be reassigned. These become compile-time constants when conditions are met. For reference types, final freezes the reference—the pointer cannot change—while the underlying object remains mutable. This produces runtime immutable references.
Identifying Compile-Time Constants
Compile-time constants are resolved during compilation and embedded directly into bytecode. Consider this validation:
public class ConstantDemo {
public final int instanceVal = 10; // ❌ Not a compile-time constant (missing static)
public static final int STATIC_VAL = 20; // ✓ Compile-time constant
public static final Integer WRAPPED = 30; // ❌ Wrapper types never qualify
public static final Status CODE = Status.OK; // ❌ Enums excluded
public final int delayedInit; // ❌ Constructor-initialized
public static final long DERIVED = STATIC_VAL - 5; // ✓ Expression of constants
ConstantDemo(int seed) { this.delayedInit = seed; }
static { System.out.println("Class initialized"); }
}
Demonstration:
public class ConstantAccess {
public static void main(String[] args) {
System.out.println(ConstantDemo.STATIC_VAL); // No initialization triggered
System.out.println(ConstantDemo.DERIVED); // No initialization triggered
System.out.println(ConstantDemo.WRAPPED); // Triggers <clinit>
System.out.println(ConstantDemo.CODE); // Triggers <clinit>
}
}
Output:
20
15
Class initialized
30
OK
Accessing WRAPPED or CODE forces class initialization, confirming they are not compile-time constants.
Bytecode Evidence
Using javap -c reveals that instanceVal receives its value in instance constructors. Each object instantiation duplicates this constant in memory—an inefficient pattern. True compile-time constants require the static modifier for shared storage.
Compile-time constants live in the constant pool and get inlined into bytecode. Only primitive types and Strings qualify. Wrapper classes, enums, and arrays never do.
JLS Definition
Java Language Specification §15.29 defines constant expressions recursively:
- Primitive or String literals
- Type casts to primitives or String
- Unary operators
+,-,~,!applied to constants - Multiplicative, additive, shift, relational, equality, bitwise, and logical operators on constants
- Conditional operator
?:with constant operands - Parenthesized constant expressions
- Simple names referring to constant variables
- Qualified names of the form
TypeName . Identifierfor constant fields
Counterexample:
public static final int RANDOM = new SecureRandom().nextInt(); // ❌ Method invocation disqualifies
Class Initialization Mechanics
Class loading spans three phases:
- Loading: Reading binary class data into the VM
- Linking: Verification (structural checks), preparation (default values for static fields), resolution (symbolic to direct references)
- Initialization: Executing
<clinit>to assign proper initial values
The <clinit> method synthesizes all static field initializers and static blocks. Critically: if every static final field is a compile-time constant, no <clinit> method generates.
Initialization Triggers
A class initializes upon first active use:
- Instantiation via
new, reflection, cloning, or deserialization - Invocation of static methods
- Reading or writing static fields (except
static finalcompile-time constants) - Reflection operations on class members
- Initialization of subclasses
- Designation as the main class
Practical Applications
Thread-Safe Publication Immutable constants enable safe unsynchronized sharing across threads.
JVM Optimization HotSpot aggressively caches and inlines constants.
Inner Class Capture Local classes access only effective final variables from enclosing scopes:
private void executeTransaction() {
final var payload = fetchData();
final var metadata = getContext();
txTemplate.execute(status -> {
repository.save(payload, metadata);
return null;
});
}
Constant Folding JIT compilers evaluate constant expressions at compile time:
static int compute() {
final int width = 8;
final int height = 12;
return width * height; // Compiled as: return 96;
}
This eliminates runtime arithmetic entirely.