

Derived Record Creation Expressions (oder kurz: Withers) sind eine prägnante Schreibweise, um von Java-Records abgeleitete Records zu erzeugen, die sich in einem oder mehreren (oder auch keinen) Feldern von dem ursprünglichen Record unterscheiden.
In diesem Artikel erfährst du:
- Warum brauchen wir Derived Record Creation Expressions (Withers)?
- Was sind explizite Wither-Methoden?
- Wie funktionieren Derived Record Creation Expressions?
- Welche Einschränkungen gelten bei der Verwendung von Derived Record Creation Expressions?
Derived Record Creation Expressions werden in einer der kommenden Java-Versionen als Preview-Feature veröffentlicht. Welche Java-Version das sein wird, ist aktuell noch nicht klar, da sich das entsprechende JDK Enhancement Proposal 468 noch im Candidate-Status befindet.
Warum brauchen wir Withers?
Java Records sind unveränderbar – und das ist gut so. Denn Unveränderbarkeit (Immutability) macht Code verständlicher, verlässlicher und sicherer. Es gibt aber immer wieder Use-Cases, in denen wir von einem bestehenden Record einen neuen Record ableiten wollen, der sich von dem bestehenden Record in nur einem oder einigen Feldern unterscheidet.
Das möchte ich dir an einem Beispiel zeigen – und zwar an folgenden Record:
public record Point3D(double x, double y, double z) { }
Code-Sprache: Java (java)
Nehmen wir an, wir haben einen bestehenden Point3D point
und wollen nun nur die Z-Koordinate um 10,0 erhöhen. Dann müssen wir wie folgt einen neuen Record erzeugen:
Point3D pointNew = new Point3D(point.x(), point.y(), point.z() + 10.0);
Code-Sprache: Java (java)
Wir müssen also aus dem bestehenden Record alle Felder auslesen und dann für den neuen Record wiederum alle Felder angeben – die geänderten und auch die nicht geänderten. Zum einen ist das aufwändig, und zum anderen kann das – besonders bei komplexeren Records – schnell fehleranfällig werden.
Wäre es nicht schön, wenn wir nur diejenigen Felder angeben müssten, die sich geändert haben?
Bisherige Lösung: Explizite Wither-Methoden
Eine Möglichkeit, um die Arbeit für die „Benutzer:innen“ des Records zu erleichtern, ist die Bereitstellung sogenannter Wither-Methoden. Das sind Methoden innerhalb des Records, die einen abgeleiteten Record mit einem (oder mehreren) geänderten Feldern zurückliefern.
In unserem Point3D
könnten wir beispielsweise folgende Wither-Methoden zur Verfügung stellen:
public record Point3D(double x, double y, double z) {
public Point3D withX(double newX) {
return new Point3D(newX, y, z);
}
public Point3D withY(double newY) {
return new Point3D(x, newY, z);
}
public Point3D withZ(double newZ) {
return new Point3D(x, y, newZ);
}
}
Code-Sprache: Java (java)
Das ermöglicht es uns nun, die Z-Koordinate wie folgt zu ändern:
Point3D pointNew = point.withZ(point.z() + 10.0);
Code-Sprache: Java (java)
Nachteile von expliziten Wither-Methoden
Explizite Wither-Methoden haben zwei Nachteile.
Der erste ist offensichtlich:
Wir müssen eine Menge Boilerplate-Code implementieren, und das stellt einen erhöhten Implementierungs- und Wartungsaufwand dar.
Der zweite Nachteil:
Falls es semantische Einschränkungen über die Kombination mehrerer Felder eines Records gibt, können diese Felder ggf. nicht einzeln geändert werden. Nehmen wir einmal an, die Entfernung unseres Point3D
vom Ausgangspunkt des Koordinatensystems darf nicht größer als 100,0 sein.
Das könnten wir relativ einfach mit folgendem kompakten Konstruktor sicherstellen:
public record Point3D(double x, double y, double z) {
public Point3D {
double distance = Math.sqrt(x * x + y * y + z * z);
if (distance > 100.0) {
throw new IllegalArgumentException("Point lies outside the allowed distance " +
"of 100 units from origin (0, 0, 0).");
}
// . . .
}
Code-Sprache: Java (java)
Nehmen wir nun an, wir haben einen Punkt mit den Koordinaten (0, 80, 10) und wollen dessen X- und Y-Koordinaten vertauschen:
Point3D point = new Point3D(0, 80, 10);
Point3D pointNew = point
.withX(point.y())
.withY(point.x());
Code-Sprache: Java (java)
Leider führt das zu einer IllegalArgumentException
, da im ersten Schritt – also beim Aufruf von withX(point.y())
– versucht wird, einen Punkt mit den Koordinaten (80, 80, 10) zu erstellen.
Wir müssten also in Point3D
eine weitere Wither-Methode zur Verfügung stellen, die sowohl X- als auch Y-Koordinate setzt (eine sogenannte Compound Wither Method):
public record Point3D(double x, double y, double z) {
// . . .
public Point3D withXY(double newX, double newY) {
return new Point3D(newX, newY, z);
}
// . . .
}
Code-Sprache: Java (java)
Folgender Aufruf wäre dann erfolgreich:
Point3D point = new Point3D(0, 80, 10);
Point3D pointNew = point.withXY(point.y(), point.x());
Code-Sprache: Java (java)
Falls wir möglicherweise andere Koordinatenpaare tauschen wollen, bräuchten wir entsprechend weitere Wither-Methoden. Bei komplexeren Records wird das schnell unübersichtlich und fehleranfällig.
Die Lösung: Derived Record Creation Expressions
Wäre es nicht schöner, wenn wir den ganzen Boilerplate-Code vermeiden und uns darauf konzentrieren könnten, anzugeben, welche Record-Komponenten sich auf welche Weise ändern sollen?
JEP 468 wird mit Derived Record Creation Expressions genau das möglich machen!
Zunächst einmal entfernen wir alle expliziten Wither-Methoden aus unserem Record – der sieht nun wieder wie ganz zu Beginn aus:
public record Point3D(double x, double y, double z) { }
Code-Sprache: Java (java)
Um jetzt z. B. die Z-Koordinate um 10,0 zu erhöhen, können wir ganz einfach das neue with
-Keyword verwenden:
Point3D pointNew = point with {
z += 10;
};
Code-Sprache: Java (java)
Das bedeutet: „Erzeuge einen neuen Record mit z
um 10,0 erhöht“. Dieser Code ist deutlich lesbarer und wartbarer als alles vorherige, da er sich nur auf das fokussiert, was sich ändert, und keinen weiteren Boilerplate-Code erfordert.
Noch ein paar weitere Beispiele...
x
und y
vertauschen könnten wir wie folgt:
Point3D pointNew = point with {
double helper = x;
x = y;
y = helper;
};
Code-Sprache: Java (java)
Alle Koordinaten mit 2,0 multiplizieren könnten wir so:
Point3D pointNew = point with {
x *= 2.0;
y *= 2.0;
z *= 2.0;
};
Code-Sprache: Java (java)
Wir können auch das with
-Keyword mehrfach aufrufen – die Multiplikation aller Koordinaten mit 2,0 wäre auch wie folgt möglich:
Point3D pointNew = point
with { x *= 2.0 }
with { y *= 2.0 }
with { z *= 2.0 };
Code-Sprache: Java (java)
Aber Achtung: Die letzten zwei Beispiele sind nicht identisch! Im ersten wird ein neuer Record erzeugt. Im zweiten werden drei neue Records erzeugt – bei jedem with
-Aufruf einer. Das ist zum einen mehr Aufwand, und zum anderen könnten dabei Validierungen fehlschlagen, die sich auf die Kombination mehrerer Felder beziehen.
Mehr dazu im nächsten Abschnitt.
Wie genau funktioniert Derived Record Creation?
Ich werde die genaue Funktionsweise von Derived Record Creation Expressions am ersten with
-Beispiel von oben erklären – hier ist es noch einmal:
Point3D pointNew = point with {
z += 10;
};
Code-Sprache: Java (java)
Eine Derived Record Creation Expression besteht aus drei Teilen:
- der Origin Expression (dem „Ursprungsausdruck“) – im Beispiel:
point
- dem Keyword
with
- dem Transformation Block – im Beispiel:
{ z += 10; }
Innerhalb des Transformationsblocks werden alle Felder des ursprünglichen Records durch Aufruf von dessen Accessor-Methoden in lokalen, änderbaren Variablen bereitgestellt. Es ist so, als würde vor der Ausführung des Transformationsblocks folgender Code ausgeführt werden:
double x = x();
double y = y();
double z = z();
Code-Sprache: Java (java)
Es wird also nicht direkt auf die Felder zugegriffen: Sollten die Accessor-Methoden überschrieben worden sein und weitere Logik enthalten, so wird diese ausgeführt.
Dann wird der Transformationsblock ausgeführt – im Beispiel also die lokale Variable z
um 10,0 erhöht.
Am Ende des Transformationsblocks wird ein neuer Record anhand der (möglicherweise veränderten) lokalen Variablen erzeugt, also so:
new Point3D(x, y, z)
Code-Sprache: Java (java)
Dabei wird auch tatsächlich der Record-Konstruktor aufgerufen, um dort möglicherweise vorhandene Validierungen auszuführen.
Damit verstehst du auch, warum die letzten beiden Beispiele des vorherigen Abschnitts unterschiedlich sind: Beim einmaligen Aufruf von with
wird der Konstruktor einmal aufgerufen – mit allen Änderungen. Beim mehrmaligen Aufruf von with
wird der Konstruktor mehrfach
aufgerufen – jeweils mit nur einer Änderung. So würden Validierungen, die sich auf die Kombination mehrerer Parameter beziehen, bei einem ungültigen Zwischenzustand fehlschlagen.
Hier noch einige Hinweise:
- Sollte der ursprüngliche Ausdruck – im Beispiel also
point
–null
sein, kommt es zu einerNullPointerException
. - Der Transformationsblock darf auch leer sein – in dem Fall wird eine unveränderte Kopie des ursprünglichen Records zurückgegeben.
- Falls außerhalb der Derived Record Creation Expression eine Variable existiert mit demselben Namen wie ein Feld des Records, dann ist diese innerhalb des Transformationsblocks nicht sichtbar (sie ist „geshadowed“).
Hier ist der dritte Punkt nochmal an einem Beispiel erklärt:
double x = 50;
Point3D point = new Point3D(10, 20, 30);
Point3D pointNew = point with {
// x is 10 here, not 50.
// The "outer" x is not visible here.
x = 20;
}
// x is 50 here, not 20.
// The "inner" x is not visible here.
Code-Sprache: Java (java)
Verschachtelte Derived Record Creation
Bei ineinander verschachtelten Records können auch die with
-Ausdrücke verschachtelt werden. Der folgende Record definiert eine Linie im dreidimensionalen Raum:
public record Line3D(Point3D start, Point3D end) { }
Code-Sprache: Java (java)
Legen wir einmal eine solche Linie an:
Line3D line = new Line3D(new Point3D(1, 2, 3), new Point3D(4, 5, 6));
Code-Sprache: Java (java)
Dann könnten wir jetzt z. B. den Endpunkt wie folgt ändern (noch nicht verschaltelt):
line = line with {
end = new Point3D(4, 5, 10);
};
Code-Sprache: Java (java)
Der neue Endpunkt unterscheidet sich nur in der Z-Koordinate vom ursprünglichen Endpunkt. Das können wir auch wie folgt – mit verschachtelten with
-Ausdrücken – prägnanter schreiben:
line = line with {
end = end with { z = 10; }
};
Code-Sprache: Java (java)
Innere Derived Record Creation
Derived Record Creation Expressions dürfen auch innerhalb des Records eingesetzt werden. Beispielsweise könnten wir für den Point3D
-Record eine Skalierungsmethode anbieten:
public record Point3D(double x, double y, double z) {
// . . .
public Point3D scale(double factor) {
return this with {
x *= factor;
y *= factor;
z *= factor;
};
}
}
Code-Sprache: Java (java)
Einschränkungen von Derived Record Creation Expressions
Für Derived Record Creation Expressions gelten folgende Einschränkungen:
- Der Transformationsblock darf keinen
return
-Ausdruck enthalten. - Der Transformationsblock darf keinen
yield
-,break
- odercontinue
-Ausdruck enthalten, dessen Ziel außerhalb des Transformationsblocks liegt.
Den zweiten Punkt erkläre ich auch noch einmal an einem Beispiel.
Erlaubt ist z. B. der folgende (zugegebenermaßen stark konstruierte) Code, in dem die Ziele von yield
und break
innerhalb des Transformationsblocks liegen:
Point3D point = new Point3D(10, 20, 30);
Point3D pointNew = point with {
y = switch (x) {
case double d when d < 0.0 -> -1;
case double d when d > 0.0 -> {
double newValue = d;
for (int i = 0; i < 10; i++) {
newValue *= 2.0;
if (newValue > 100.0) break; // allowed
}
yield newValue; // allowed
}
default -> 0;
};
}
Code-Sprache: Java (java)
Folgender (ebenso stark konstruierter) Code hingegen wäre nicht erlaubt, da hier das Ziel von break
die for
-Schleife außerhalb der Derived Record Creation Expression wäre:
Point3D point = new Point3D(10, 20, 30);
for (int i = 0; i < 10; i++) {
point = point with {
if (x > 0.0) {
x -= 1.0;
} else {
break; // not allowed
}
};
}
Code-Sprache: Java (java)
Fazit
Derived Record Creation Expressions sind eine prägnante Syntax, um abgeleitete Records zu erstellen und dabei nur die geänderten Felder zu spezifizieren – ohne dafür selbst explizite, wartungsintensive Wither-Methoden schreiben zu müssen.
Aktuell ist leider noch nicht abzusehen, wann Derived Record Creation verfügbar sein wird – sobald sich das ändert, wirst du es hier frühzeitig erfahren.