Understanding the Java final Keyword: Compile-Time Constants and Runtime Immutability

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 . Identifier for constant fields

Counterexample:

public static final int RANDOM = new SecureRandom().nextInt(); // ❌ Method invocation disqualifies

Class Initialization Mechanics

Class loading spans three phases:

  1. Loading: Reading binary class data into the VM
  2. Linking: Verification (structural checks), preparation (default values for static fields), resolution (symbolic to direct references)
  3. 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 final compile-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.

Tags: java final keyword compile-time constants JVM class initialization

Posted on Tue, 06 Oct 2026 16:51:38 +0000 by Thivya