liveBook · Manning

Circular dependency

As explained in 100 Java Mistakes and How to Avoid Them

A circular dependency is a dependency cycle in which class initialization directly or indirectly requires initialization of a class that depends back on the original class.

A circular dependency in class initialization occurs when initializing one class or enum directly or indirectly needs another type that leads back to the first. The cycle can expose fields before their initializers finish, producing surprising default values such as null rather than the intended value. section 3.9.3 section 3.9.4

Static fields can observe incomplete initialization

If A initializes B before assigning A.WELCOME, and B initializes MSG from A.WELCOME, B may read WELCOME as null. Field declaration order matters; a primitive or String compile-time constant is initialized during class loading instead, before ordinary class initialization begins. section 3.9.3

The more serious failure appears under concurrency. The JVM locks a class while its initializer runs so that it is not executed twice. Two threads that begin opposite sides of a cycle can each hold one class-initialization lock and wait for the other, deadlocking the application. Later accesses to either class can also block. section 3.9.3

Treat initialization-time cycles as defects, especially when types may be reached concurrently:

  • Do not make static field initializers cross a dependency cycle; move or restructure the dependency so a type is not consumed before initialization completes. section 3.9.3
  • Inspect a stuck process with jstack. Frames in <clinit> identify class initializers; Java 17 and newer can additionally report the class-initialization monitor being awaited. section 3.9.3
  • Do not assume enum constants already exist while their enum initializes. An enum constructor can observe a referenced constant from another enum as null. section 3.9.4

Mutually referring enums

Pet.DOG can receive Voice.BARK successfully while Voice.BARK.owner becomes null, because Pet.DOG was not ready when Voice.BARK was constructed. Where a back-reference is necessary, declare it nonfinal and assign it after constructor invocation. section 3.9.4

Enum initialization also makes an enum temporarily incomplete: values() does not work until every constant exists. In Java 20 and older, switching on this inside an enum constructor may implicitly invoke values() and fail with a NullPointerException in the class initializer. section 3.9.4

enum Pet {
  DOG(Voice.BARK), 
  CAT(Voice.MEOW);

  final Voice voice;
  Pet(Voice voice) { this.voice = voice; }
}

enum Voice {
  BARK(Pet.DOG),
  MEOW(Pet.CAT);

  final Pet owner;
  Voice(Pet owner) { this.owner = owner; }
}

This pair of enums isolates the chicken-and-egg cycle: each constant passes a constant from the other enum into its constructor. section 3.9.4

How 100 Java Mistakes and How to Avoid Them covers it

How deep it goes
Explains field ordering, class-initialization locks, enum construction, and implicit switch behavior; defers broader dependency-design techniques.
Examples
Uses mutually initializing classes, parallel construction of A and B, jstack output, mutually referring enums, and an enum constructor switch.
What it assumes
Assumes Java familiarity with classes, static fields, threads, enums, constructors, and switch expressions.
What you can do after
Diagnose/debug with jstack class-initializer frames and recognize initialization cycles that can yield nulls or deadlock.
How it connects
Connects circular dependency to static field initialization, compile-time constants, deadlock, enums, switch expressions, and jstack.

Also explained in

Compare how 5 books explain circular dependency →

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.