Zum Inhalt springen

Flexible Constructor Bodies (JEP 513): Code vor super() in Java 25

Stufenpyramide aus Travertin auf Jadegrün – auf den drei Stufen links hinauf liegt je eine bernsteinfarben leuchtende Glaskugel, oben ein Goldblock, auf den drei Stufen rechts hinab je eine weiß leuchtende Kugel

In diesem Artikel erfährst du:

  • wie du ab Java 25 (ab Java 22 als Preview-Feature) Code in Konstruktoren auch vor dem Aufruf von super(...) oder this(...) ausführen kannst,
  • welche Einschränkungen es dabei gibt,
  • was Prolog und Epilog eines Konstruktors sind,
  • ob die Neuerungen auch für Records und Enums gelten,
  • und was zu tun ist, wenn der Compiler das Feature mit einem Fehler ablehnt.

Gehen wir einen Schritt zurück: Warum sollte man Code vor super(...) oder this(...) ausführen wollen?

Code in Konstruktoren – Status Quo vor Java 25

Die folgenden Beispiele zeigen zum einen Workarounds, die bis Java 24 erforderlich waren, um vor dem Aufruf von super(...) oder this(...) Parameter zu validieren oder zu berechnen – und zum anderen, was schiefgehen konnte, wenn der Konstruktor der Elternklasse eine Methode aufruft, die in der Kindklasse überschrieben wird.

Use Case 1: Validierung von Parametern

Ein häufiger Use Case ist die Validierung von Parametern einer Kindklasse. Im folgenden Beispiel ruft der Konstruktor von Rectangle erst den Konstruktor der Elternklasse, Shape, auf und validiert und setzt danach die Breite und Höhe:

public class Shape {
  private final Color color;

  public Shape(Color color) {
    this.color = color;
  }
}

public class Rectangle extends Shape {
  private final double width;
  private final double height;

  public Rectangle(Color color, double width, double height) {
    super(color);
    if (width < 0 || height < 0) throw new IllegalArgumentException();
    this.width = width;
    this.height = height;
  }
}

Effizienter wäre es allerdings, die Parameter zu validieren, bevor der Super-Konstruktor aufgerufen wird. Doch das war bis Java 24 nur mit dem folgenden, extrem unschönen Workaround möglich:

public Rectangle(Color color, double width, double height) {
  super(validateParams(color, width, height));
  this.width = width;
  this.height = height;
}

private static Color validateParams(Color color, double width, double height) {
  if (width < 0 || height < 0) throw new IllegalArgumentException();
  return color;
}

Use Case 2: Berechnung eines Arguments, das an mehrere Parameter übergeben wird

Ein weiterer Use Case ist die Berechnung von Werten, die an mehr als einen Superklassen-Konstruktorparameter weitergegeben werden sollen. Im folgenden Beispiel wollen wir ein Quadrat mit vorgegebener Fläche erzeugen (dass eine statische Factory-Methode mit aussagekräftigem Namen dafür geeigneter wäre als der Konstruktor, wollen wir an dieser Stelle ignorieren):

public class Square extends Rectangle {
  public Square(Color color, int area) {
    super(color, Math.sqrt(area), Math.sqrt(area));
  }
}

Um die Wurzel der Fläche nicht zweimal zu berechnen, müssten wir einen Hilfskonstruktor einführen:

public class Square extends Rectangle {
  public Square(Color color, int area) {
    this(color, Math.sqrt(area));
  }

  private Square(Color color, double sideLength) {
    super(color, sideLength, sideLength);
  }
}

Das ist hier aber auch nur deshalb möglich, weil area vom Typ int ist. Wäre area wie sideLength vom Typ double, würde das nicht funktionieren, da wir dann zwei Konstruktoren mit identischer Signatur hätten.

Und wollten wir zuvor sichergehen, dass area nicht negativ ist, müssten wir eine dritte Methode einführen, da wir auch vor this(...) keinen anderen Code ausführen dürfen:

public class Square extends Rectangle {
  public Square(Color color, int area) {
    this(color, Math.sqrt(validateArea(area)));
  }

  private static double validateArea(int area) {
    if (area < 0) throw new IllegalArgumentException();
    return area;
  }

  private Square(Color color, double sideLength) {
    super(color, sideLength, sideLength);
  }
}

Es ist kaum noch ersichtlich, was dieser Code tut.

Use Case 3: Aufruf einer überschriebenen Methode im Super-Konstruktor

Wir bleiben beim Shape/Rectangle-Beispiel und fügen eine printMe()-Methode hinzu, die im Konstruktor von Shape aufgerufen und in Rectangle überschrieben wird:

public class Shape {
  private final Color color;

  public Shape(Color color) {
    this.color = color;
    printMe();
  }

  void printMe() {
    System.out.println("color = " + color);
  }
}

public class Rectangle extends Shape {
  private final double width;
  private final double height;

  public Rectangle(Color color, double width, double height) {
    super(color);
    if (width < 0 || height < 0) throw new IllegalArgumentException();
    this.width = width;
    this.height = height;
  }

  @Override
  void printMe() {
    super.printMe();
    System.out.println("width = " + width + ", height = " + height);
  }
}

Wenn wir nun z. B. new Rectangle(Color.RED, 29.7, 21.0) aufrufen, dann wird nicht etwa color = RED und width = 29.7, height = 21.0 ausgegeben, sondern:

color = RED
width = 0.0, height = 0.0

Der Grund dafür ist, dass printMe() vom Shape-Konstruktor aufgerufen wird, bevor im Rectangle-Konstruktor width und height initialisiert werden. printMe() sieht also noch die Default-Werte von width und height, nämlich jeweils 0,0.

Java-Code vor super(...) und this(...)

In Java 22 wurde – zunächst als Preview-Feature und unter dem Namen „Statements before super(...)“ – die Möglichkeit eingeführt, Code auch vor dem Aufruf von super(...) oder this(...) auszuführen.

Wir können damit zunächst die Validierung der Fläche vor den Aufruf von this(...) ziehen:

public class Square extends Rectangle {
  public Square(Color color, int area) {
    if (area < 0) throw new IllegalArgumentException();  // ⟵ Validation before `this`
    this(color, Math.sqrt(area));
  }

  private Square(Color color, double sideLength) {
    super(color, sideLength, sideLength);
  }
}

Und auch den Hilfskonstruktor brauchen wir nicht mehr. Parametervalidierung und Berechnung der Seitenlänge können nun direkt im Konstruktor untergebracht werden:

public Square(Color color, int area) {
  if (area < 0) throw new IllegalArgumentException();  // ⟵ Validation before `super`
  double sideLength = Math.sqrt(area);                 // ⟵ Calculation before `super`
  super(color, sideLength, sideLength);
}

Bei diesem Konstruktor ist auf einen Blick erkennbar, was der Code macht.

In einem überarbeiteten Preview in Java 23 wurde zudem die Möglichkeit geschaffen, vor dem Aufruf von super(...) Felder der Klasse zu initialisieren. Wir dürfen damit die Rectangle-Klasse so schreiben:

public class Rectangle extends Shape {
  private final double width;
  private final double height;

  public Rectangle(Color color, double width, double height) {
    this.width = width;    // ⟵ Field initialization before `super`
    this.height = height;  // ⟵ Field initialization before `super`
    super(color);
  }

  . . .
}

Bei einem Aufruf von new Rectangle(Color.RED, 29.7, 21.0) liefert die vom Konstruktor aufgerufene printMe()-Methode nun die erwartete Ausgabe:

color = RED
width = 29.7, height = 21.0

Warum funktioniert das jetzt? Der Prolog – also this.width = width; und this.height = height; – wird vollständig ausgeführt, bevor super(color) und damit der Shape-Konstruktor läuft. Wenn dieser nun printMe() aufruft, sind width und height bereits gesetzt. Bis einschließlich Java 22 war genau das unmöglich: Zuweisungen an Felder standen zwangsläufig nach super(...) – und damit zu spät.

Allgemein gilt: Beim Erzeugen eines Objekts werden zuerst die Prologe von der Kindklasse aufwärts bis hinauf zu Object abgearbeitet, danach wird jeder Konstruktor ab dem Aufruf von super(...) ausgeführt, von Object abwärts. Felder, die du im Prolog setzt, stehen also schon bereit, wenn der Super-Konstruktor zu arbeiten beginnt.

Flexible Constructor Bodies sind seit Java 25 als finalisiertes Feature verfügbar.

Konstruktor-Prolog und -Epilog

Der Block vor dem Aufruf von super(...) oder this(...) in einem Konstruktor wird Prolog genannt.

Code nach dem Aufruf von super(...) oder this(...) oder Code in einem Konstruktor ohne Aufruf von super(...) oder this(...) wird als Epilog bezeichnet.

Das folgende Diagramm zeigt, in welcher Reihenfolge Prologe und Epiloge bei new Square(color, area) laufen. Jede Zeile ist eine Klasse, von Square unten bis Object oben. Links steht der Prolog, rechts der Epilog, in der Mitte der Aufruf, mit dem der Konstruktor den Konstruktor der Elternklasse startet. Der Konstruktor von Shape hat keinen expliziten Aufruf – vor seinem Epilog steht implizit super(), sein Prolog ist leer.

Ablauf von new Square(color, area): links die Prologe von Square (area prüfen, sideLength berechnen), Rectangle (width und height setzen) und Shape (leer) mit Pfeilen aufwärts, oben der Object-Konstruktor, rechts die Epiloge von Shape (color setzen, printMe() aufrufen), Rectangle und Square (leer) mit Pfeilen abwärts; dazwischen die Aufrufe super(…), super(color) und das implizite super()
Die Prologe laufen von Square aufwärts, die Epiloge von Object abwärts – wenn der Epilog von Shape printMe() aufruft, sind width und height schon gesetzt

Einschränkungen

Im Prolog darf der Code Felder initialisieren, aber nicht lesend auf Felder der Klasse zugreifen und keine nicht-statischen Methoden der Klasse aufrufen. Er darf außerdem keine Instanzen von nicht-statischen inneren Klassen erzeugen, denn diese hätten eine Referenz auf das äußere Objekt – und das ist genau die Instanz, die gerade erst konstruiert wird.

Der Prolog des Konstruktors einer inneren Klasse darf hingegen uneingeschränkt auf Felder und Methoden der äußeren Klasse zugreifen.

Zuweisen darf der Prolog nur Felder, die bei ihrer Deklaration keinen Initializer haben. Ein Feld-Initializer wie = 0 läuft – um das bisherige Verhalten nicht zu verändern – erst, nachdem der Super-Konstruktor zurückgekehrt ist, und würde den Wert aus dem Prolog wieder überschreiben. Die Felder width und height von Rectangle haben keinen Initializer – deshalb darf der Prolog sie setzen.

Die folgende Klasse versucht, ein Feld mit Initializer im Prolog zu setzen:

public class Counter {
  private int count = 0;

  public Counter(int count) {
    this.count = count;
    super();
  }
}

Der Compiler weist die Zuweisung ab:

Counter.java:5: error: assignment to initialized field 'count' may only appear after an explicit constructor invocation
    this.count = count;
        ^

So lautet die Meldung seit Java 26. Java 25 formuliert sie als cannot assign initialized field 'count' before supertype constructor has been called.

Außerdem darf der Prolog kein return enthalten und nicht über super. auf Felder oder Methoden der Elternklasse zugreifen. Eine Exception werfen darf er dagegen – das ist der Fail-fast-Fall aus dem Square-Beispiel.

Records und Enums

Records und Enums können zwar nicht von einer selbst gewählten Klasse erben, deren Konstruktoren können allerdings mit this(...) alternative Konstruktoren aufrufen.

Auch davor darf nun Code, der den oben genannten Einschränkungen standhält, ausgeführt werden.

Bei Records gilt das nur für die nicht-kanonischen Konstruktoren: Der kanonische Konstruktor darf keinen this(...)-Aufruf enthalten.

Compiler-Fehler: „Flexible Constructor Bodies is only available with source level 25 and above“

Eclipse und die Java-Erweiterung von Visual Studio Code melden für den Code von oben diesen Fehler, wenn das Source-Level des Projekts unter 25 liegt:

The Java feature 'Flexible Constructor Bodies' is only available with source level 25 and above

Die Ursache ist das Source-Level des Projekts, nicht das installierte JDK – beide IDEs verwenden den Eclipse-Compiler, der die Meldung so formuliert.

javac formuliert es anders. Bei einem Source-Level unter 25 – zum Beispiel, weil das Build-Tool noch --release 21 setzt – meldet er:

Rectangle.java:8: error: flexible constructors is not supported in -source 21
    this.width = width;
    ^
  (use -source 25 or higher to enable flexible constructors)

Auf Java 23 und 24, wo das Feature noch ein Preview war, lautet die Meldung:

Rectangle.java:8: error: flexible constructors is a preview feature and is disabled by default.
    this.width = width;
    ^
  (use --enable-preview to enable flexible constructors)

Java 22 meldet für das Rectangle-Beispiel etwas anderes – auch mit --enable-preview:

Rectangle.java:8: error: cannot reference this before supertype constructor has been called
    this.width = width;
    ^

Das Preview in Java 22 erlaubte vor super(...) nur Anweisungen, die keine Felder setzen – Zuweisungen an Felder kamen erst mit Java 23. Für das Square-Beispiel, das nur prüft und rechnet, meldet Java 22 ohne Preview-Flag:

Square.java:7: error: statements before super() is a preview feature and is disabled by default.
    super(color, sideLength, sideLength);
         ^
  (use --enable-preview to enable statements before super())

In Java 22 heißt das Feature an dieser Stelle noch „statements before super()“.

Die Lösung ist, das Source-Level auf 25 anzuheben: in Maven über die Property maven.compiler.release, in Gradle über eine Java-Toolchain mit Language Version 25, in IntelliJ über File→Project Structure→Project Settings→Project→Project language level und in Eclipse über Project→Properties→Java Compiler→Compiler compliance level. Visual Studio Code übernimmt das Level aus der Maven- oder Gradle-Konfiguration.

Wenn dein Projekt noch auf Java 23 oder 24 läuft, kannst du den Code mit --enable-preview --release <Version> übersetzen – auf Java 22 nur Code wie das Square-Beispiel, der vor super(...) keine Felder setzt. Das Programm musst du dann auch mit --enable-preview starten. Ohne dieses Flag lehnt der Class Loader die Klasse ab:

Exception in thread "main" java.lang.UnsupportedClassVersionError: Preview features are not enabled for Rectangle (class file version 68.65535). Try running with '--enable-preview'

Die 68 steht für Java 24. Mit Java 23 übersetzte Klassen melden 67, mit Java 22 übersetzte 66.

Historie

Flexible Constructor Bodies wurden in folgenden JDK Enhancement Proposals definiert:

Fazit

Das Ausführen von Code vor super(...) oder this(...) erlaubt es, Felder zu initialisieren und Parameter zu validieren oder zu berechnen, bevor der Super-Konstruktor oder ein alternativer Konstruktor aufgerufen wird. Das macht den Code sicherer und ermöglicht deutlich ausdrucksstärkeren Code als die Workarounds, die wir bis Java 24 für solche Zwecke konstruieren mussten.

Flexible Constructor Bodies wurden in Java 25 finalisiert und stehen seitdem ohne Preview-Flag zur Verfügung. Falls du noch Java 23 oder 24 einsetzt, kannst du das Feature dort als Preview mit --enable-preview --release <Version> aktivieren. Java 22 erlaubt im Preview nur Anweisungen vor super(...), noch keine Zuweisungen an Felder.

Was soll ich als Nächstes vertiefen? Das erfahre ich am besten durch dein Feedback: Eine Bewertung auf meinem ProvenExpert-Profil zeigt mir, welche Themen dir wichtig sind – und motiviert mich, weitere Artikel zu schreiben.

👉 Bewertung abgeben

Dieses Thema im eigenen Code?

Du hast den Artikel gelesen – im Training arbeitet ihr damit. 2 Tage lang gehen wir die Themen an euren eigenen Projekten durch, statt an konstruierten Beispielen.

Praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Java 25 SchulungAlle Trainings ansehen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche