In diesem Artikel erfährst du:
- wie du ab Java 22 Code in Konstruktoren auch vor dem Aufruf von
super(...)
oderthis(...)
ausführen kannst, - welche Einschränkungen es dabei gibt,
- was Prolog und Epilog eines Konstruktors sind,
- und ob die Neuerungen auch für Records und Enums gelten.
Gehen wir einen Schritt zurück: Warum sollte man Code vor super(...)
oder this(...)
aufrufen wollen?
Code in Konstruktoren – Status Quo vor Java 22
Die folgenden Beispiele zeigen zum einen Workarounds, die bisher erforderlich waren, um vor dem Aufruf von super()
oder this()
Parameter zu validieren oder zu berechnen – und zum anderen, was schief gehen 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 Konstruktur 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;
}
}
Code-Sprache: Java (java)
Effizienter wäre es allerdings, die Parameter zu validieren, bevor der Super-Konstruktor aufgerufen wird. Doch das ist bisher nur mit dem folgenden, extrem unschönen Workaround möglich:
public Rectangle(Color color, int width, int height) {
super(validateParams(color, width, height));
this.width = width;
this.height = height;
}
private static Color validateParams(Color color, int width, int height) {
if (width < 0 || height < 0) throw new IllegalArgumentException();
return color;
}
Code-Sprache: Java (java)
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));
}
}
Code-Sprache: Java (java)
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);
}
}
Code-Sprache: Java (java)
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);
}
}
Code-Sprache: Java (java)
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);
}
}
Code-Sprache: Java (java)
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
Code-Sprache: Klartext (plaintext)
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
, also jeweils 0,0.
Java-Code vor super(...) und this(...)
Mit JDK Enhancement Proposal 447 wurde in Java 22 – 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(...)
aufzurufen.
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);
}
}
Code-Sprache: Java (java)
Und auch den Hilfs-Konstruktor 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);
}
Code-Sprache: Java (java)
Bei diesem Konstruktor ist auf einen Blick erkennbar, was der Code macht.
Mit JDK Enhancement Proposal 482 wurde in Java 23 darüber hinaus 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);
}
. . .
}
Code-Sprache: Java (java)
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
Code-Sprache: Klartext (plaintext)
Konstruktor-Prolog und -Epilog
Der Block vor dem Aufruf von super(...)
oder this(...)
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.
Einschränkungen
Im Prolog darf der Code Felder initialisieren, aber nicht lesend auf Felder der Klasse zugreifen und keine nicht-statische Methoden der Klasse aufrufen. Er darf außerdem keine Instanzen von nicht-statischen inneren Klassen erzeugen, da diese dann eine Referenz auf das potentiell uninitialisierte Elternobjekt haben würden.
Der Prolog des Konstruktors einer inneren Klasse darf hingegen uneingeschränkt auf Felder und Methoden der äußeren Klasse zugreifen.
Records und Enums
Records und Enums können zwar keine Elternklasse haben, 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.
Fazit
Der Aufruf 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 bisher für solche Zwecke konstruieren mussten.
Musstest du auch schon komplizierte Workarounds implementieren, und wie findest du das neue Feature? Lass es mich über die Kommentarfunktion wissen!
Du willst über alle neue Java-Features auf dem Laufenden sein? Dann klicke hier, um dich für den HappyCoders-Newsletter anzumelden.