Java Stable Values JEP 502 Feature ImageJava Stable Values JEP 502 Feature Image
HappyCoders Glasses

Lazy Constants in Java - Endlich Werte sicher initialisieren!

Sven Woltmann
Sven Woltmann
Aktualisiert: 29. November 2025

Lazy Constants (in Java 25 hießen sie noch noch Stable Values) sind Werte, die zur Laufzeit einer Anwendung nur ein einziges Mal zugewiesen werden können – dies aber zu einem beliebigen Zeitpunkt – und danach konstant bleiben. Sie standardisieren die verzögerte Initialisierung („lazy initialization“) von Konstanten und erlauben der JVM, diese Konstanten so zu optimieren, wie sie es auch für finale („final“) Werte tun kann.

In diesem Artikel erfährst du:

  • Was sind Lazy Constants und wie benutzt man sie?
  • Welche Vorteile bringt die Unveränderlichkeit einer Lazy Constant?
  • Wie haben wir Unveränderlichkeit bisher implementiert, und welche Nachteile hatte das?
  • Was sind Lazy Lists und Lazy Maps?
  • Wie funktionieren Lazy Constants intern?

Lazy Constants sind ein Preview-Feature, das in Java 25 unter dem Namen Stable Values veröffentlicht wurde (JDK Enhancement Proposal 502) und in Java 26 deutlich vereinfacht und in Lazy Constants umbenannt wurde (JEP 526).

Im den ersten Abschnitten erkläre ich, warum wir Lazy Constants überhaupt brauchen. Falls du dir das schon denken kannst, dann springe gerne direkt zum Abschnitt „Die Lösung: Lazy Constants“.

Warum Immutability (Unveränderlichkeit)?

Im der Einführung habe ich erklärt, dass es sich bei Lazy Constants um Werte handelt, die nur einmal zugewiesen werden, danach aber unveränderlich bleiben. Aber was bringt es uns, wenn Werte unveränderlich („immutable“) sind? Die Unveränderlichkeit bringt einige Vorteile mit sich:

1. Ein unveränderliches Objekt kann problemlos von mehreren Threads genutzt werden.

Es besteht keine Gefahr von Race Conditions, die wir bei veränderlichen Objekten nur durch Synchronisierung oder Memory Barriers verhindern können. Dabei schleichen sich selbst bei erfahrenen EntwicklerInnen leicht Fehler ein.

2. Die JVM kann unveränderliche Objekte optimieren, z. B. durch Constant Folding.

Wenn die JVM z. B. erkennt, dass an mehreren Stellen auf serviceRegistry.userService() zugegriffen wird, und sie weiß, dass serviceRegistry konstant ist und userService() eine Konstante zurückgibt, dann kann sie alle Aufrufe von serviceRegistry.userService() durch die userService-Konstante ersetzen.

3. Unveränderliche Objekte machen den Code besser lesbar.

Code ist vorhersehbarer, leichter verständlich und leichter zu debuggen, wenn man sich keine Gedanken über mögliche Änderungen von Objektzuständen machen muss. Bei veränderlichen Objekten sollten wir für Parameter und Rückgabewerte defensive Kopien erstellen, um sicherzustellen, dass diese nicht versehentlich modifiziert werden. Bei unveränderlichen Objekten ist das nicht notwendig.

Immutability mit „final“

Bisher war die einzige Möglichkeit, um Immutability zu erreichen, Felder eines Objekts mit final zu kennzeichnen. Statische finale Felder müssen bei der Deklaration oder in einem static-Block zugewiesen werden und werden beim Laden der Klasse initialisiert. Finale Instanzfelder müssen bei der Deklaration oder im Konstruktor zugewiesen werden und werden beim Erzeugen eines neuen Objekts der Klasse initialisiert.

Im folgenden Beispiel wird ein statisches Logger-Feld initialisiert:

public class UserService {
  private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);

  // . . .
}Code-Sprache: Java (java)

Alternativ mit einem static-Block:

public class UserService {
  private static final Logger LOGGER;

  static {
    LOGGER = LoggerFactory.getLogger(UserService.class);
  }

  // . . .
}Code-Sprache: Java (java)

Im folgenden Beispiel wird für jedes neue Task-Objekt eine unveränderliche UUID generiert:

public class Task {
  private final UUID taskId = UUID.randomUUID();

  // . . .
}Code-Sprache: Java (java)

Alternativ im Konstruktor:

public class Task {
  private final UUID taskId;

  public Task() {
    taskId = UUID.randomUUID();
  }

  // . . .
}Code-Sprache: Java (java)

Nicht immer ist die Initialisierung von Konstanten ganz so einfach. Im Folgenden zeige ich dir einige weniger triviale Beispiele.

Verzögerte Initialisierung („Lazy Initialization“)

Finale Felder werden in jedem Fall initialisiert, selbst wenn sie gar nicht (oder erst sehr viel später) benutzt werden. Wenn aber die Initialisierung, also z. B. das Erzeugen des Loggers, eine Weile dauert (weil dieser z. B. eine Verbindung zu einem externen Logging-System aufbaut), der Logger dann aber nie (oder erst später) im Programmablauf verwendet wird, wurde der Start der Anwendung durch die frühzeitige Initialisierung u. U. unnötig verlangsamt.

Felder, die teuer zu initialisieren sind, können wir verzögert, also erst bei Bedarf („lazy“) initialisieren. In einer single-threaded Anwendung ist das einfach:

public class UserService {
  private static Logger logger;

  // Not thread-safe!!!
  private static Logger getLogger() {
    if (logger == null) {
      logger = LoggerFactory.getLogger(UserService.class);
    }
    return logger;
  }

  // . . .
}Code-Sprache: Java (java)

Ein zweiter Use Case:

Nicht immer haben wir beim Erzeugen eines Objekts alle Informationen vorliegen, die wir für die Initialisierung eines unveränderlichen Feldes benötigen. Beispielsweise könnte ein Service erzeugt werden, bevor eine Verbindung zur Datenbank besteht – der Service muss aber zur Initialisierung eines Feldes auf die Datenbank zugreifen.

Auch so ein Feld können wir lazy initialisieren:

public class BusinessService {
  private Settings settings;

  // Not thread-safe!!!
  private Settings getSettings() { 
    if (settings == null) {
      settings = loadSettingsFromDatabase();
    }
    return settings;
  }

  // . . .
}Code-Sprache: Java (java)

In einer Spring- oder Jakarta-EE-Anwendung könnten wir die settings-Variable auch in einer mit @PostConstruct annotierten Methode initialiseren:

@Service
public class BusinessService {
  private Settings settings;

  @PostConstruct
  private void initializeSettings() {
    settings = loadSettingsFromDatabase();
  }

  // . . .
}Code-Sprache: Java (java)

Doch all dies sind Workarounds, und sie haben einige entscheidende Nachteile. Welche das sind, erfährst du im folgenden Abschnitt.

Nachteile der „hausgemachten“ Lazy Initialization

Wenn wir uns die Beispiele aus dem vorherigen Abschnitt noch einmal anschauen, dann fällt auf: Die Felder logger und settings sind nicht mehr als final gekennzeichnet. Denn das geht nur, wenn sie bei der Deklaration, in einem static-Block oder im Konstruktor initialisiert werden.

Das wiederum bedeutet: Wir können nicht garantieren, dass die Felder nach der Initialisierung nicht doch noch verändert werden. Und ohne die Garantie, dass die Werte unveränderlich sind, kann die JVM kein Constant Folding durchführen.

Außerdem müssen wir – zumindest bei den ersten zwei Beispielen – sicherstellen, dass wir auf die Felder nie direkt, sondern immer über die getLogger() bzw. getSettings()-Methode zugreifen.

Und wenn wir uns eben diese Methoden noch einmal anschauen, dann stellen wir fest: Sie sind (bisher) nicht threadsicher! Sie dürfen also nicht aus mehreren Threads heraus aufgerufen werden.

Um die getSettings()-Methode thread-safe zu machen, könnten wir sie mit synchronized markieren:

private synchronized Settings getSettings() {
  if (settings == null) {
    settings = loadSettingsFromDatabase();
  }
  return settings;
}Code-Sprache: Java (java)

Das macht sie zwar threadsicher, gleichzeitig aber auch die Anwendung deutlich langsamer, da nun bei jedem Zugriff auf die Settings der synchronized-Block betreten werden muss.

Schneller (aber auch fehleranfälliger) ist das sogenannte Double-checked Locking:

private volatile Settings settings; // ⟵ `settings` must be volatile!

private Settings getSettings() {
  Settings localRef = settings;
  if (localRef == null) {
    synchronized (this) {
      localRef = settings;
      if (localRef == null) {
        settings = localRef = loadSettingsFromDatabase();
      }
    }
  }
  return localRef;
}Code-Sprache: Java (java)

Warum du hierbei auf keinen Fall das volatile vergessen darfst und welchen Zweck die zusätzliche (auf den ersten Blick überflüssige) Variable localRef hat, kannst du im Artikel über das Double-checked Locking Idiom nachlesen.

Eine Alternative ist das sogenannte Initialization-on-Demand Holder Idiom, bei dem die Tatsache ausgenutzt wird, dass die JVM Klassen zum einen lazy und zum anderen threadsicher lädt. Auch das ist ein Workaround. Nicht alle kennen ihn, und er funktioniert nur bei statischen Feldern, nicht bei Instanzfeldern.

Zusammengefasst:

  1. Verzögert („lazy“) initialisierte Werte können nicht als final markiert werden; die Unveränderlichkeit ist also nicht garantiert.
  2. Dementsprechend kann die JVM den Code nicht durch Constant Folding optimieren.
  3. Der Aufruf eines verzögert initialisierten Wertes muss immer über eine Hilfsmethode erfolgen.
  4. In Multithreading-Anwendungen muss diese Hilfsmethode threadsicher sein. Hier können sich leicht Fehler einschleichen, was zu subtilen Race Conditions führt.

Was uns in Java fehlt, ist ein Mittelweg zwischen final und veränderbar. Ein Wert, der dann initialisiert wird, wenn er benötigt wird. Ein Wert, der auf jeden Fall nur einmal initialisiert wird. Und ein Wert, der auch dann korrekt initialisiert wird, wenn aus mehreren Threads auf ihn zugegriffen wird.

Und genau dieser Mittelweg sind Lazy Constants!

Die Lösung: Lazy Constants

Eine Lazy Constant ist ein Container, der ein Objekt enthält, den sogenannten „Inhalt“ (englisch: „content“). Eine Lazy Constants wird genau einmal initialisiert, bevor ihr Inhalt abgerufen wird; danach ist sie unveränderlich. Eine Lazy Constant ist thread-safe, d. h. wenn auf sie von mehreren Threads aus zugegriffen wird, wird sie maximal einmal initialisiert. Und die JVM kann eine Lazy Constant genauso gut durch Constant Folding optimieren wie ein finales Feld.

Im Folgenden siehst du, wie du das Settings-Beispiel mit einer Lazy Constant implementieren kannst.

public class BusinessService {
  private final LazyConstant<Settings> settings =
      LazyConstant.of(this::loadSettingsFromDatabase);

  public Locale getLocale() {
    return settings.get().getLocale(); // ⟵ Here we access the lazy constant
  }

  // . . .
}
Code-Sprache: Java (java)

Die an LazyConstant.of() übergebene Methode – hier die Methode, die die Settings aus der Datenbank lädt – wird Berechnungsfunktion („Computing Function“) genannt. Beim ersten Aufruf der get()-Methode wird der Inhalt der Lazy Constant einmalig initialisiert, indem die Berechnungsfunktion aufgerufen wird.

Das von of() zurückgelieferte LazyConstant-Objekt könntest du übrigens auch in einem Supplier speichern, da LazyConstant von Supplier erbt. LazyConstant bietet allerdings neben get() noch zwei weitere Methoden:

  • isInitialized() – gibt zurück, ob der Wert bereits initialisiert wurde.
  • orElse(T other) – gibt den berechneten Wert zurück, falls initialisiert, andernfalls other.

Beide Methoden führen nicht zu einer Initialisierung der Lazy Constant.

Lazy Lists

Wir können nicht nur einzelne Lazy Constants definieren, sondern auch eine Liste von Lazy Constants, also eine Liste, bei der jedes einzelne Element erst beim Zugriff darauf – z. B. mit first(), get(int index) oder last() – initialisiert wird.

Das folgende Beispiel erzeugt eine Lazy List, in der jedes Element bei dessen ersten Aufruf mit der Quadratwurzel des Listenindexes initialisiert wird:

List<Double> squareRoots = List.ofLazy(100, Math::sqrt);Code-Sprache: Java (java)

Die Größe der Liste und deren Elemente sind nicht änderbar. Die Methoden add(), set() und remove() führen zu einer UnsupportedOperationException. Abgeleitete Listen – z. B. mit subList() oder reversed() – sind ebenfalls Lazy Lists.

Hier ein kleines Demo-Programm (ich verwende hier eine vereinfachte Main-Methode, die es ab Java 21 als Preview-Feature gibt, und die in Java 25 finalisiert wurde):

void main() {
  List<Double> squareRoots = List.ofLazy(100, i -> {
    IO.println("Initializing list element at index " + i);
    return Math.sqrt(i);
  });

  IO.println("squareRoots[0]    = " + squareRoots.get(0));
  IO.println("squareRoots[1]    = " + squareRoots.get(1));
  IO.println("squareRoots[2]    = " + squareRoots.get(2));
  IO.println("squareRoots[0]    = " + squareRoots.get(0));
  IO.println("squareRoots.first = " + squareRoots.getFirst());
  IO.println("squareRoots.last  = " + squareRoots.getLast());
}Code-Sprache: Java (java)

Das Programm gibt folgendes aus:

Initializing list element at index 0
squareRoots[0]    = 0.0
Initializing list element at index 1
squareRoots[1]    = 1.0
Initializing list element at index 2
squareRoots[2]    = 1.4142135623730951
squareRoots[0]    = 0.0
squareRoots.first = 0.0
Initializing list element at index 99
squareRoots.last  = 9.9498743710662Code-Sprache: Klartext (plaintext)

Du kannst hier gut erkennen, dass die Lazy List das Element an Position 0 nur einmal berechnet, obwohl es drei Mal abgerufen wird (zwei Mal mit get(0) und ein Mal mit getFirst()).

Lazy Map

Analog zu Lazy Lists können wir auch Lazy Maps erzeugen. Bei einer Lazy Map wird für jeden Key der zugehörige Value erst beim ersten Abruf initialisisert und dann gespeichert.

Das folgende Beispiel zeigt eine Lazy Map, mit der wir Lokalisierungsresourcen pro Sprache dynamisch beim ersten Aufruf laden können:

Set<Locale> supportedLocales = getSupportedLocales();
Map<Locale, ResourceBundle> resourceBundles =
    Map.ofLazy(supportedLocales, this::loadResourceBundle);Code-Sprache: Java (java)

Erst beim ersten Aufruf von resourceBundles.get(...) wird das entsprechende Resource Bundle über die als Methodenreferenz übergebene loadResourceBundle(...)-Methode geladen.

Wie funktionieren Lazy Constants intern?

Lazy Constants sind ausschließlich im Java-Code implementiert. Änderungen an Compiler, Bytecode oder JVM waren nicht erforderlich, wie aus dem Pull Request für JEP 502 hervorgeht.

Der Inhalt einer Lazy Constants wird in einem nicht-finalen Feld gespeichert. Dieses ist mit der JDK-internen Annotation @Stable versehen, die auch an anderen Stellen des JDK-Codes zur Optimierung eingesetzt wird. Eben diese Annotation sagt der JVM, dass sich der Wert nach der Initialisierung nicht ein weiteres Mal ändern wird. Und so kann die JVM, nachdem der Wert gesetzt wurde, mit der Constant-Folding-Optimierung loslegen.

Die Threadsicherheit wird durch Memory Barriers sichergestellt, die über die Unsafe-Klasse gesetzt werden.

Heißt das, LazyConstant ist im Grunde genommen nur ein Wrapper, den wir auch selbst implementieren könnten?

Ja, aber...

Erstens können wir weder die JDK-interne @Stable-Annotation noch die interne Unsafe-Klasse verwenden, ohne diese explizit über --add-exports java.base/jdk.internal.vm.annotation bzw. --add-exports java.base/jdk.internal.misc unserem Modul zur Verfügung zu stellen.

Zweitens sollten wir diese JDK-Internals nicht verwenden, da nicht garantiert ist, dass diese sich nicht in einem späteren Java-Release ändern werden.

Und drittens schreiben wir ja auch z. B. eine ConcurrentHashMap nicht selbst. Dadurch, dass LazyConstant von JDK-Spezialisten implementiert wird, können wir sichergehen, dass alle nur bekannten Performance-Tricks angewendet wurden und dass auch in Zukunft weitere Performance-Optimierungen vorgenommen werden. Und wenn LazyConstant in Zukunft durch Millionen von Java-Entwickler:innen genutzt wird, können wir auch sicher sein, dass eventuelle Bugs – selbst subtile Concurrency-Bugs – schnell gefunden und behoben werden.

Fazit

Lazy Constants sind Konstanten, die zu jeder beliebigen Zeit „on demand“ initialisiert werden können. Danach sind sie immutable und werden von der JVM genau wie finale Felder behandelt, also z. B. durch Constant Folding optimiert.

Lazy Constants sind threadsicher, können also auch in Multithreading-Programmen eingesetzt werden, ohne subtile Concurrency Bugs zu riskieren.

Neben Lazy Constants gibt es Lazy Lists und Lazy Maps, die die Elemente in Listen und Maps einmalig initialisieren und dann unveränderlich speichern.

Lazy Constants sind als Preview-Feature in Java 25 (dort noch unter dem Namen „Stable Values“) und im aktuellen Early-Access-Build von Java 26 enthalten.

Was hälst du von Lazy Constants? Teile deine Meinung in den Kommentaren!

Java-Schulungen
(online oder vor Ort)
»