liveBook · Manning

Constant expression

As explained in 100 Java Mistakes and How to Avoid Them

A compile-time constant is a final value whose initializer can be evaluated by the compiler and embedded where it is referenced.

A constant expression is evaluated during compilation rather than at runtime. In Java, a primitive or String field initializer that is a compile-time constant expression is initialized during class loading, before ordinary class initialization begins. This timing difference can change the result of circular static initialization. section 3.9.3

A method call prevents early initialization

In a circular dependency, `static final String WELCOME = "Hello".toUpperCase()` is not a compile-time constant expression. If `B.MSG` reads `A.WELCOME` while A is still initializing, it can see `null`. Replacing that initializer with the literal `"Hello"` makes it available during class loading instead. section 3.9.3

class A {
  private static final B unused = new B();    #1
  static final String WELCOME = "Hello".toUpperCase();

  public static void main(String[] args) {
    System.out.println(B.MSG);
  }
}
class B {
  static final String MSG = A.WELCOME;     #2

  public static void main(String[] args) {
    System.out.println(B.MSG);
  }
}

The two classes isolate the ordering hazard: A creates B before assigning WELCOME, while B reads WELCOME during its own initialization. section 3.9.3

Compile-time evaluation also hides arithmetic operations from bytecode-based analysis. An overflowing integer constant expression is already reduced by compilation, so a tool such as SpotBugs cannot recover the original computation; IntelliJ IDEA can flag the source-level numeric overflow. section 4.2.1

Overflow computed before bytecode

`int microseconds = 24 * 60 * 60 * 1000 * 1000;` exceeds the `int` range, but the constant computation silently overflows. When variables participate, static analyzers are generally much less able to prove overflow. section 4.2.1

Treat compile-time constants as both an initialization and analysis boundary:

  • Do not rely on a nonconstant static initializer being visible during circular class initialization; remove the cycle or make initialization order explicit. section 3.9.3
  • Inspect integer literal calculations for range overflow, especially multiplication, rather than expecting bytecode analysis to catch them. section 4.2.1
  • Keep format strings constant when possible, because static analysis can validate format arguments only when the format string is a constant. section 6.11

How 100 Java Mistakes and How to Avoid Them covers it

How deep it goes
Distinguishes constant from method-call initialization and traces its class-loading consequence; notes source-versus-bytecode analysis limits.
Examples
Uses mutually initializing A and B classes and an overflowing microseconds literal.
What it assumes
Assumes familiarity with Java static fields, field initializers, integer types, and basic static analysis.
What you can do after
Evaluate/understand when a Java initializer changes class-loading behavior or conceals an arithmetic computation from bytecode analysis.
How it connects
Connects compile-time constants with class initialization, circular dependencies, integer overflow, static analysis, and format strings.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Java Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.