java withers - derived record creationjava withers - derived record creation
HappyCoders Glasses

Withers in Java: Derived Record Creation Expressions
(Preview)

Sven Woltmann
Sven Woltmann
Aktualisiert: 9. Juni 2026

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?

JDK Enhancement Proposal 468 wird mit Derived Record Creation Expressions genau das möglich machen!

Beachte:
Derived Record Creation Expressions sind aktuell noch in keiner Java-Version verfügbar, d. h. du musst dich noch etwas gedulden, bis du den folgenden Code ausprobieren kannst.

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 funktionieren Derived Record Creation Expressions?

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:

  1. der Origin Expression (dem „Ursprungsausdruck“) – im Beispiel: point
  2. dem Keyword with
  3. 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 pointnull sein, kommt es zu einer NullPointerException.
  • 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 ist hier 10, nicht 50.
    // Das „äußere“ x ist hier nicht sichtbar.
    x = 20;
};

// x ist hier 50, nicht 20.
// Das „innere“ x is hier nicht sichtbar.Code-Sprache: Java (java)

Verschachtelte Derived Record Creation Expressions

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 verschachtelt):

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 keine return-Anweisung enthalten.
  • Eine Derived Record Creation Expression ist kein „statement expression" – sie darf nicht als eigenständige Anweisung verwendet werden.
  • Steht auf der linken Seite einer Zuweisung ein einfacher (unqualifizierter) Name, muss dieser entweder eine der implizit angelegten Komponenten-Variablen oder eine im Block selbst deklarierte Variable sein.
  • Der Transformationsblock darf keine yield-, break- oder continue-Anweisung enthalten, deren Ziel außerhalb des Transformationsblocks liegt.

Die return-Einschränkung ist selbsterklärend. Die übrigen drei schauen wir uns genauer an – von der einfachsten zur umfangreichsten Regel.

Am einfachsten ist die Regel, dass eine Derived Record Creation Expression kein „statement expression" ist. Du kannst sie also nicht als eigenständige Anweisung verwenden, indem du ein Semikolon anhängst. Ihr Ergebnis muss weiterverwendet werden – z. B. einer Variablen zugewiesen, an eine Methode übergeben oder aus einer Methode zurückgegeben:

Point3D point = new Point3D(10, 20, 30);

point with { x = 0; };                     // nicht erlaubt: Ergebnis wird nicht verwendet
Point3D pointNew = point with { x = 0; };  // erlaubtCode-Sprache: Java (java)

Als Nächstes die Zuweisungsregel. Eine äußere Variable kannst du im Transformationsblock zwar auslesen, ihr aber über ihren einfachen Namen keinen Wert zuweisen:

int counter = 0;

Point3D point = new Point3D(10, 20, 30);
Point3D pointNew = point with {
    double offset = counter;  // erlaubt: counter (äußere Variable) darf gelesen werden
    x = x + offset;           // erlaubt: x ist eine Komponenten-Variable,
                              //          offset eine im Block deklarierte Variable
    counter = 1;              // nicht erlaubt: counter ist eine äußere Variable
};Code-Sprache: Java (java)

Die Kontrollfluss-Einschränkung erkläre ich an ein paar Beispielen. Ich verwende dafür einen neuen Record: einen Helden mit Level und Erfahrungspunkten (XP):

public record Hero(int level, int xp) { }Code-Sprache: Java (java)

Erlaubt ist z. B. der folgende Code, in dem das Ziel des break – die while-Schleife – innerhalb des Transformationsblocks liegt:

Hero hero = new Hero(98, 0);
Hero afterQuest = hero with {
    xp += 350;
    while (xp >= 100) {
        if (level >= 99) break;  // Ziel des break ist die while-Schleife im Block → erlaubt
        level += 1;
        xp -= 100;
    }
};Code-Sprache: Java (java)

Aus Hero(98, 0) wird hier Hero(99, 250): Der Held steigt auf Level 99 auf, erreicht den Level-Cap, und die restlichen 250 XP bleiben liegen.

Ebenfalls erlaubt ist ein yield, dessen Ziel – der switch-Ausdruck – innerhalb des Blocks liegt:

Hero hero = new Hero(10, 0);
Hero rewarded = hero with {
    xp += switch (level) {
        case 1, 2, 3 -> 50;           // Anfänger-Level: feste XP
        default -> {
            int reward = level * 20;  // höhere Level: XP abhängig vom Level
            yield reward;             // Ziel des yield ist der switch im Block → erlaubt
        }
    };
};Code-Sprache: Java (java)

Nicht erlaubt ist hingegen der folgende Code, da hier das Ziel des break die äußere for-Schleife – also außerhalb der Derived Record Creation Expression – wäre:

Hero hero = new Hero(98, 0);
for (int quest = 0; quest < 10; quest++) {
    hero = hero with {
        if (level < 99) {
            level += 1;
        } else {
            break;  // nicht erlaubt: Ziel ist die äußere for-Schleife
        }
    };
}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.

Java-Schulungen
(online oder vor Ort)
»