Destructoring ist die Zukunft von Javas Encapsulation

Richard Gross

Encapsulation in Java ist seit dem Java Development Kit (JDK) 5 kaputt. Objekte sollten garantieren, dass ihr innerer Zustand zu dem passt, was wir im Code ausdrücken, aber zur Laufzeit kann jeder private– und final-Zustand von außen auf beliebige Werte gesetzt werden – unter Umgehung aller Prüfungen und Assertions. Möglich ist das über die Reflection-API, mit der man jedes private Feld per setAccessible zugänglich machen und anschließend den Wert eines beliebigen final Feldes mit set setzen kann. Diese spezielle Verwendung der Reflection-API, um ein unveränderliches final Feld zu verändern, wird Deep Reflection genannt.

Deep Reflection erzeugt eine Diskrepanz zwischen dem, was der Code aussagt, und dem, was zur Laufzeit tatsächlich passiert. Beispielsweise könnte der Code garantieren, dass ein Zahlenfeld immer größer als Null ist oder dass ein E-Mail-Feld stets eine gültige Adresse enthält; wegen Deep Reflection können wir uns zur Laufzeit aber nicht darauf verlassen. Das Resultat sind einige der verwirrendsten Bugs, die häufig nur unter speziellen Umständen auftreten.

Einige Entwicklungen im JDK werden Deep Reflection nach und nach zurückrollen, die Encapsulation reparieren und sicherstellen, dass der Code, den wir lesen, auch dem entspricht, der ausgeführt wird. Einiges ist bereits in JDK 15 gelandet, manches kommt in JDK 26 (März 2026), anderes wird noch etwas dauern. Eine dieser Entwicklungen ist Destructoring — das Zerlegen eines Objekts in seine Bestandteile — und es ist der Schlüssel zu Javas Fähigkeit, Encapsulation und Invarianten zuverlässig aufrechtzuerhalten. Es ist noch nicht im JDK und wird dort auch noch eine Weile nicht auftauchen, aber die bisher offen diskutierten Pläne sind faszinierend. Die folgenden Kapitel erläutern, wie Destructoring funktionieren könnte, warum es so viele Probleme im JDK löst und wie es die Encpasulation reparieren würde.

Encapsulation

Encapsulation means that a code element guarantees it is always in a valid state.

Oracle Blog: Quiz Yourself

Encapsulation (Kapselung) bedeutet insbesondere die Garantie aller Invarianten — Aussagen über das Element, die immer wahr sind. Betrachten wir den folgenden Code. Die modellierte Invariante lautet: „Adults sind mindestens 18 Jahre alt“. In bestimmten Situationen druckt displayAdult jedoch 17 auf System.out. Es gibt eine Diskrepanz zwischen dem, was der Code sagt, und dem, was zur Laufzeit passiert.

public class Adult implements Person {  
    private final Instant birthdate;  

    public Adult(Instant birthdate) {  
        this.birthdate = birthdate;  
        if(isYoungerThan18(birthdate))  
            throw new IllegalArgumentException("Person is not 18 yet.");  
    }  

    public long age(){ /** **/ }
}


void displayAdult(Adult adult){  
    IO.println(adult.age()); // prints 17 ?? 
}

Der Grund für die gebrochene Altersinvariante ist eine kundenspezifische Datenmigration, die Reflection verwendet. Die Migration begrenzt das Alter fehlerhafter Child‑Objekte — fehlerhafte Daten, die wir korrigieren sollten — korrigiert aber tatsächlich alle Persons (auch Adults) via Reflection. Das bricht die Encapsulation.

Reflection

Die Reflection-API (java.lang.reflect) erlaubt Programmen, zur Laufzeit das Verhalten von Klassen zu untersuchen und zu ändern. Sie bietet programmgesteuerten Zugriff auf Felder, Methoden und Konstruktoren geladener Klassen. Das ermöglicht dynamische Aufrufe, Feldzugriffe und Objektinstanzen ohne Compile‑Time‑Wissen. Mit Reflection kann man mit Klassen/Methoden arbeiten, die erst zur Laufzeit bekannt sind. Das ist sehr nützlich für Debugger, Frameworks und Serialisierungsbibliotheken. Letztere sind allerdings die Ursache der meisten Encapsulation-verletzungen – nicht aus eigenem Verschulden, sondern wegen des Features, das dem JDK zur Unterstützung der Serialisierung hinzugefügt wurde: Deep Reflection. Das sieht so aus:

void main(){
    // given an adult
    var seventeenYearsAgo = now.minusYears(17);  
    var eighteenYearsAgo = now.minusYears(18);  
    var adult = new Adult(eighteenYearsAgo); 
    // when using reflection
    Field birthdateField = adult.getClass().getDeclaredField("birthdate");
    birthdateField.setAccessible(true);  
    birthdateField.set(adult, seventeenYearsAgo);
    // then  
    IO.println(adult.age()); // prints 17
}

Reflection ist seit JDK 1.1 verfügbar. Seit JDK 1.2 können wir mit setAccessible ein privates Feld zugänglich machen und ein nicht‑final Feld mit set setzen. Die Fähigkeit, final Felder über Reflection zu verändern, wurde erstmals in JDK 5 hinzugefügt, „damit Drittanbieter‑Serialisierungsbibliotheken eine Funktionalität auf Augenhöhe mit der Serialisierung des JDK selbst bereitstellen konnten“ (“so that third-party serialization libraries could provide functionality on par with the JDK’s own serialization facilities”). Eine Entscheidung, die das JDK bis heute verfolgt.

Da setAccessible und set nicht nur unrechtmäßigen Zugriff auf Klassenmitglieder, sondern sogar auf JDK‑Interna erlauben, wird diese Kombination stark abgeraten und trägt den Namen Deep Reflection. Deep Reflection ist einer von vielen Blockern, nicht nur für die Wartbarkeit des JDK, sondern auch für das sogenannte Integrity by Default. Die JDK‑Maintainer beschreiben dieses Ziel so:

Developers expect that their code and data is protected against use that is unwanted or unwise. The Java Platform, however, contains unsafe APIs that can undermine this expectation, thereby damaging the correctness, maintainability, scalability, security, and performance of applications. Going forward, we will restrict the unsafe APIs so that, by default, libraries, frameworks, and tools cannot use them.

Integrity by Default

Über die Jahre hinweg wurden viele Verbesserungen im JDK ausgeliefert, um den Standard in Richtung Integrität zu verändern. Eine davon war die Einführung von records. Während Klassen die Mutation von finalen Feldern erlauben, tun Records das von Anfang an nicht. Das ist ein großer Vorteil, nicht nur für uns Entwickler — wir können darauf vertrauen, dass Record‑Felder tatsächlich unveränderlich sind — sondern auch, weil diese Felder „für JIT‑Optimierungen vertrauenswürdig“ sind. Da Record‑Felder wirklich final sind, kann die Java Virtual Machine (JVM) eine Optimierung einmal durchführen und darauf vertrauen, dass sie gültig bleibt. Bei classes muss die JVM kontinuierlich prüfen, ob sich der Wert geändert hat, und die Optimierung gegebenenfalls neu durchführen.

Offensichtlich sollte Deep Reflection also nicht der Standard sein. Wenn überhaupt, dann sollte es uns Entwicklern überlassen sein, ob wir es aktivieren. Prepare to make final mean final ist eine Änderung, die mit JDK 26 (März 2026) ausgeliefert wird und uns auf eine solche Welt vorbereitet. Sie deaktiviert Deep Reflection nicht, “gibt aber Warnungen für Verwendungen von Deep Reflection zum Mutieren finaler Felder aus”.

Wenn Integrität der Standard sein soll, warum wurde die encapsulation-brechende Deep Reflection so lange im JDK beibehalten und warum ist sie nicht standardmäßig deaktiviert? Weil sie für kritische Anwendungsfälle weit verbreitet ist, für die es bislang keine sichere Alternative gibt. Einer davon ist die Serialisierung bzw. Drittanbieter‑Serialisierung. Solange wir für diese kritischen Anwendungsfälle keine Alternativen haben, können wir Deep Reflection nicht standardmäßig abschalten. Sehen wir uns an, wie sie funktionieren.

Serialisierung

Java’s Serialization feature has garnered several years worth of security exploits and zero day attacks, earning it the nickname, “the gift that keeps on giving” and “the fourth unforgivable curse”.

State of Java Serialization

Ziel der Serialization‑API war ein schlanker Mechanismus, um die Repräsentation eines Objekts über Sockets zu teilen oder ein Objekt mitsamt Zustand zur späteren Wiederherstellung zu speichern (Deserialisierung). Sie wurde in den Tagen der Common Object Request Broker Architecture (CORBA) und des Java‑Äquivalents — Remote Method Invocation (RMI) — entworfen und ist seit JDK 1.1 dabei.

Brian Goetz, Java Language Architect bei Oracle, drückt es folgendermaßen aus: “(The Serialization Api) was probably critical to Java’s success — Java would probably not have risen to dominance without it, as serialization enabled the transparent remoting that in turn enabled the success of Java EE. Serialisierung selbst ist nicht das Problem.

Das Problem ist das Design der Serialization‑API. Da die API querschnittlich ist, muss sie bei fast allen neuen Java‑Features mitbedacht werden und kann überall im JDK Sicherheitslücken verursachen. Möglicherweise war Serialisierung bei fast der Hälfte aller JDK‑Schwachstellen involviert. Sie ist “the gift that keeps on giving” – und funktioniert wie folgt: Mit ObjectOutputStream.writeObject() kann man ein Objekt in eine Datei oder einen Socket serialisieren, mit ObjectInputStream.readObject() deserialisieren und das Java‑Objekt zurückbekommen. Sehr einfach zu benutzen, also offensichtlich ein gutes Design, oder?

void main(){      
    var adult = new Adult(...);
    // create ObjectOutputStream
    try (var stream = new ObjectOutputStream(Files.newOutputStream(aFile))) {  
        // serialize file
        stream.writeObject(adult);  
    }
}

public class Adult implements Serializable {  

    @Serial  
    private static final long serialVersionUID = 1L;

    private final Instant birthdate;  

    public Adult(Instant birthdate) {  
        this.birthdate = birthdate;  
        if(isYoungerThan18(birthdate))  
            throw new IllegalArgumentException("Person is not 18 yet.");  
    }  

    public long age(){ /** **/ }
}

Dieses Design ist in vielerlei Hinsicht fehlerhaft und wurde in zahlreichen Artikeln diskutiert, darunter einer mit dem schönen Titel “serialization must die”. Der größte Kritikpunkt in diesem Kontext ist, dass die API für die Serialisierung alle Felder verschlingt — einschließlich privater — und bei der Deserialisierung den Konstruktor umgeht. Wie Reflection bricht Serialisierung die Encapsulation. Es sei denn, man verwendet ein record. Hier schreibt die Spezifikation den Aufruf des kanonischen Konstruktors vor. Die Record‑Designer wollten ganz offensichtlich die Encapsulation ihres neuen Sprachkonstrukts nicht kompromittieren.

Wenn die API so schlecht ist, sollten wir sie dann vielleicht nie verwenden? Es gibt tatsächlich langfristige Pläne, die Serialization‑API aus dem JDK zu entfernen. Das Problem ist natürlich, dass es im JDK keine sichere Alternative gibt und dass alle ernsthaften Anwendungen irgendeine Form von Serialisierung benötigen. Daten werden immer in irgendeiner Form „übers Kabel“ geschickt. Wir brauchen einen Weg, von Java‑Objekten in die Außenwelt und wieder zurückzukommen. Deshalb gibt es Drittanbieter‑Serialisierungsbibliotheken und Deep Reflection überhaupt erst.

Drittanbieter-Serialisierung

The ability to mutate final fields via deep reflection was added in JDK 5 so that third-party serialization libraries could provide functionality on par with the JDK’s own serialization facilities. The JDK can deserialize an object from an input stream even if the object’s class declares final fields. It does this by bypassing the class’s constructors, which ordinarily assign instance fields, and assigning values from the input stream to instance fields directly — even if they are final. Third-party serialization libraries use deep reflection to do the same.

JEP 500: Prepare to Make Final Mean Final

Seit JDK 5 können Drittanbieter‑Serialisierungsbibliotheken final Felder setzen, vorausgesetzt setAccessible(true) war erfolgreich. Ein Bugreport (JDK-5044412) dokumentiert diese bewusste Änderung. Durch diese Anpassung können Drittanbieter‑Serialisierung tief in die Eingeweide eines Objekts blicken und die Encapsulation brechen — fast wie die Serialization‑API selbst.

Ein wesentlicher Unterschied ist, dass Reflection den Konstruktor nicht umgehen kann, wenn Objekte erzeugt werden. Historisch verlangten viele Bibliotheken allerdings einen No‑Arg‑Konstruktor. Das bedeutet, dass etwaige Parameterprüfungen im Konstruktor umgangen wurden, die Encapsulation weiterhin gebrochen war, und wir wieder bei der gleichen Magie landeten, die die Serialization‑API betrieb.

Das wird sich ändern. Mit JEP 500, das in JDK 26 ausgeliefert wird, löst Deep Reflection Warnungen aus. Sobald eine Alternative existiert, wird ein zukünftiges JEP wahrscheinlich standardmäßig Fehler auslösen, und wir Entwickler müssen Deep Reflection explizit aktivieren. Der Standard wird sich in Richtung Integrität verschieben. Die bloße Existenz von APIs, die final Felder verändern können, macht es unmöglich, dem Wert eines solchen Felds zu vertrauen.

Einige Bibliotheken sind bereits auf eine Zukunft vorbereitet, in der final wirklich final heißt, andere nicht. Hibernate verlangt zum Beispiel nach wie vor einen No‑Arg‑Konstruktor für Entities. Jackson dagegen kann ganz normal über den regulären Konstruktor deserialisieren, ohne Deep Reflection und ohne Annotationen (wir vergleichen Hibernate und Jackson, um unterschiedliche Ansätze zur Serialisierung zu zeigen, nicht weil ORMs und JSON‑Mapper in irgendeiner Weise gleichwertig wären). Annotation‑frei funktioniert das bei Jackson 2.x mit dem Compiler‑Argument -parameters und dem ParameterNamesModule. Ohne das Parameter‑Flag würde Reflection die ursprünglichen Parameternamen street und city nicht sehen, sondern nur arg0 und arg1.

Das folgende Beispiel zeigt die Serialisierung (mapper.writeValueAsString) und Deserialisierung (mapper.readValue) in Aktion.

class Address {  
  
    private final String street;  
    private final String city;  
  
    public Address(String street, String city) {  
        this.street = notBlank(street);  
        this.city = notBlank(city);  
    }  
  
    public String getStreet(){ return this.street; }  
    public String getCity(){ return this.city; }  
}  
  
String notBlank(String value){  
    if (value == null || value.isBlank()) {  
        throw new IllegalArgumentException("Value must not be blank");  
    }  
    return value;  
} 

void main() throws JsonProcessingException {  
    var original = new Address("Fancy Bvd", "Fancy Town");  
    var mapper = JsonMapper.builder()  
		    // add module 
            .addModule(new ParameterNamesModule())  
            .build();  
      
    // serialize
    var json = mapper.writeValueAsString(original); 
    // deserialize
    var copy = mapper.readValue(json, Address.class); 
      
    assert Objects.equals(original.getStreet(), copy.getStreet()); 
    assert Objects.equals(original.getCity(), copy.getCity());  
}

Diese Serialisierung ist der der Serialization‑API haushoch überlegen, weil der Konstruktor verwendet wird und die Encapsulation erhalten bleibt. Versuchen wir, etwas mit leerer Straße zu deserialisieren, etwa {"street":"","city":"Cheap Town"}, dann findet unser regulärer Konstruktor den Fehler und wirft eine Exception.

Mit einem record wird das Ganze noch knapper:

record Address(String street, String city) {  
    Address {  
        notBlank(street);  
        notBlank(city);  
    }  
}  

static void notBlank(String value){  
    if (value == null || value.isBlank()) {  
        throw new IllegalArgumentException("Value must not be blank");  
    }  
}  

void main() throws JsonProcessingException {  
    var original = new Address("Fancy Bvd", "Fancy Town");  
    var mapper = JsonMapper.builder()  
            .addModule(new ParameterNamesModule())  
            .build();  

    var json = mapper.writeValueAsString(original); // serialize
    var copy = mapper.readValue(json, Address.class); // deserialize

    assert Objects.equals(original.street(), copy.street());  
    assert Objects.equals(original.city(), copy.city());  
}

Interessant an den (De-)Serialisierungsbeispielen mit class und record ist, dass die Konstruktorparameter und die Getter per Name verknüpft sein müssen. Wenn der Parameter street heißt, dann muss die zugreifende Methode getStreet() oder street() heißen (bei Records, aber auch bei Klassen, wenn man accessorNaming ohne Getter‑Präfix benutzt). Die Summe dieser Accessors ist die Inverse des Konstruktors.

In der Regel wollen wir, dass die Accessors synchron mit dem Konstruktor bleiben, weil Serialisierung häufig nicht nur in eine Richtung läuft. Man serialisiert nicht nur; man serialisiert und deserialisiert meist dieselbe Art von Objekt, weil man typischerweise denselben Typ von Objekt sendet und empfängt. Vergisst man einen Accessor, bricht die Serialisierung zur Laufzeit. Benennt man einen Konstruktorparameter um, bricht die Deserialisierung zur Laufzeit.

Dinge auseinanderzunehmen und wieder zusammenzusetzen ist miteinander verknüpft. In Java geschieht das jedoch nur zur Laufzeit und nur implizit. Java bietet lediglich eine Hälfte der Medaille. Die Lösung liegt im Nachhinein auf der Hand: Konstruktion und Dekonstruktion müssen First‑Class Citizens sein.

Dekonstruktion als First-Class Citizen

Many of the design (of the Serialization Api) stem from a common source — the choice to implement serialization by “magic” rather than giving deconstruction and reconstruction a first-class place in the object model itself.

Towards Better Serialization

Dekonstruktion ist der Prozess, ein Objekt in seine Bestandteile zu zerlegen. Für records können wir das in begrenzten Situationen bereits mit Record Patterns (JDK 21). Wir können if(obj instanceof Point(int x, int y)){} schreiben und haben innerhalb des if Zugriff auf x und y. Das ist aber keine echte Dekonstruktion. Hinter den Kulissen erledigt der Compiler lediglich Folgendes:

  1. einen instanceOf‑Check
  2. einen Cast var p = (Point) obj;
  3. eine Zuweisung über den Accessor var x = p.x()
  4. eine weitere Zuweisung var y = p.y()

Wir haben die Inverse des Konstruktors nicht definiert, wir verwenden immer noch reguläre Accessors. Es funktioniert für Records, weil Records nichts anderes sind als ihr Zustand. Konstruktor und Accessors sind verknüpft, weil der Compiler sie für uns verknüpft. Der Compiler kann auch ableiten, wie man ein Objekt zerlegt, weil er alle Accessors kennt.

Bei Klassen ist das anders. Hier haben wir viel mehr Freiheiten als Entwickler und der Compiler kann viel weniger ableiten. Der Compiler kennt zwar die internen Felder einer Klasse, weiß aber nicht, ob das auch das ist, was wir nach außen zeigen wollen. Darum habe wir auch noch kein Pattern Matching für Klassen.

Nehmen wir die folgende Timestamp‑Klasse. Ihr Konstruktor nimmt einen ISO‑8601‑Datumsstring entgegen, intern wird das aber als long gespeichert, weil das effizienter ist.

public class Timestamp {
    private final long rawValue;

    // Constructor, takes an utc date like 2026-01-13T12:31:15Z
    public Timestamp(String utcDate) {
        this.rawValue = parseUtcDateString(utcDate);
    }

    // accessor
    public String utcDate(){
        return formatAsUtcDateString(this.rawValue);
    }
}

Diese Freiheit bringen Klassen mit sich. Wir können die interne Repräsentation vollständig ändern, müssen nur die externe API (hier den Konstruktor und den Accessor) stabil halten. Klassen entkoppeln die API von der internen Repräsentation. Klassen können ihren Zustand über eine Hierarchie verteilen. Sie können abgeleiteten oder gecachten Zustand besitzen, der nicht Teil der Zustandsbeschreibung ist. Außerdem sind Klassen mutable.. Kurz gesagt: Klassen haben ihren Platz als Datenhalter und können Dinge tun, die records nicht können, weil sie deutlich weniger eingeschränkt sind. Wir können nicht einfach sagen „benutz halt Records“, weil das manchmal schlicht nicht funktioniert. Wir brauchen Dekonstruktion als Sprachkonstrukt.

Wie könnte Dekonstruktion in Java aussehen? Wie gestaltet man sie so, dass sie zum Sprachstil passt und wir „nimm mich auseinander“ und „setze mich wieder zusammen“ zusammen definieren können? In seinem Vortrag 2025 Where is the Java Language Going und dem Artikel von 2019 Towards Better Serialization skizziert Brian Goetz eine Idee (Strohmann‑Syntax, keineswegs final):

public class Timestamp {
    private final long rawValue;

    // Constructor
    public Timestamp(String utcDate) {
        this.rawValue = parseUtcDateString(utcDate);
    }

    // Deconstruction pattern
    public pattern Timestamp(String utcDate) {
        utcDate = formatAsUtcDateString(this.rawValue);
    }
}

Statt this zuzuweisen, weisen wir nun this den Ausgabewerten zu. Mir gefällt das, weil Dekonstruktion so zur visuellen Inversen der Konstruktion wird. Es ist allerdings sehr ungewohnt, einem Methodenparameter etwas zuzuweisen. Wer diese Syntax nicht mag: Andere Vorschläge findet man in Serialization – A New Hope und in Data Oriented Programming, Beyond Records. Interessant ist weniger die Syntax als das, was dadurch möglich wird.

Dieses Feature gibt die Verantwortung für die Dekonstruktion in die Hände der Person mit dem meisten Implementierungswissen: die Klassenautorin bzw. den Klassenautor. Bibliotheken müssen nicht mehr anhand von Property‑Namen erraten, wie man eine Klasse zerlegt. Das liegt komplett in der Hand des Autors. Interner Aufbau beliebig, nach außen Konstruktor und Dekonstruktions‑Pattern, und fertig. Man muss nicht raten, ob eine Klasse zerlegt werden kann; wenn es ein Dekonstruktions‑Pattern gibt, ist das beabsichtigt. Es ist alles im Code verankert und zur Compile‑Zeit überprüfbar. Das Dekonstruktions‑pattern macht viele Anwendungsfälle sicherer sowie lesbarer und lässt bisherige Methoden wie einen Hack aussehen. Der erste dieser Anwendungsfälle ist der komplette Ersatz der „verfluchten“ Serialization‑API.

(De-)Serialisierung durch (De-)Konstruktion

Ein Dekonstruktions‑Pattern ermöglicht die Serialisierung – den Prozess, in dem der Zustand eines Objekts in ein Format übersetzt wird, das gespeichert werden kann (siehe auch Wikipedia). Der Konstruktor ermöglicht die Deserialisierung – das Extrahieren eines Objekts aus einer Folge von Bytes. Während die ursprüngliche Serialization‑API zahlreiche magische Methoden und Felder kannte (readObject, writeObject, readObjectNoData, readResolve, writeReplace, serialVersionUID, serialPersistentFields) und Jackson noch mehr (@JsonAnyGetter, @JsonAnySetter, @JsonGetter, @JsonSetter, @JsonValue, @JsonRawValue, @JsonSerialize, @JsonDeserialize, @JsonCreator, @JsonAlias, @JsonIgnoreProperties, @JsonIgnore, @JsonIgnoreType, @JsonInclude, @JsonIncludeProperties etc.), könnte die neue Serialisierung im JDK nur zwei brauchen (wieder Strohmann‑Syntax):

public class Point {
    private final int x;
    private final int y;

    @Deserializer
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Serializer
    public pattern Point(int x, int y) {
        x = this.x;
        y = this.y;
    }
}

Wenn wir wollen, könnten wir das auch auf eine Factory‑Methode legen:

public class Point {
    private final int x;
    private final int y;

    private Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Serializer
    public pattern Point(int x, int y) {
        x = this.x;
        y = this.y;
    }

    @Deserializer
    public static Point of(int x, int y){
        return new Point(x, y);
    }
}

Mehrere Versionen könnten wir so behandeln:

public class Point {
    private final int x;
    private final int y;

    @Deserializer(version = 2)
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Deserializer(version = 1)
    public Point(int x) {
        this(x, 0);
    }

    @Serializer(version = 2)
    public pattern Point(int x, int y) {
        x = this.x;
        y = this.y;
    }
}

Das konkrete Serialisierungsformat (JSON, Protobuf, Datenbank‑Entität) läge in der Verantwortung der Bibliothek. Man könnte den Code einmal schreiben und dann aus beliebigen Quellen speichern und laden. Das würde Bibliotheken erheblich vereinfachen, weil der Code bereits vorgibt, wie etwas zerlegt werden soll.

Natürlich könnten wir das auch fürs Pattern Matching über Klassen und Factories nutzen.

Pattern Matching durch Dekonstruktion

Pattern Matching ist mit Records sehr mächtig, aber die Verwendung von Records bedeutet, dass man API und Implementierung eng koppelt. Außerdem kann man dann den größten Teil der Typen im JDK nicht pattern‑matchen, weil diese als Klassen modelliert sind. Sollten wir Dekonstruktions‑pattern erhalten, stünde diese Power endlich auch Klassen zur Verfügung und sähe exakt wie Record‑Patterns aus. Dekonstruktions‑Pattern deklarieren, und schon können wir darauf matchen:

class Address {  

    private final String street;  
    private final String city;  

    public Address(String street, String city) {  
        this.street = notBlank(street);  
        this.city = notBlank(city);  
    }  

    // deconstruction pattern
    public pattern Address(int street, int city) {
        street = this.street;
        city = this.city;
    } 
}  

String notBlank(String value){  
    if (value == null || value.isBlank()) {  
        throw new IllegalArgumentException("Value must not be blank");  
    }  
    return value;  
}  

void handle(Object obj) {  
    if(obj instanceOf Address(var street, var city)){ // use the deconstruction pattern
        // do something with street and city
    }
}

Ziemlich cool, und sobald das Dekonstruktions‑pattern Teil der Sprache sind, könnten wir auch Pattern Assignments bekommen, bei denen man eine bekannte Klasse oder ein Record in einer Einzeile zerlegt:

void handle(Address address){
    Address(var street, var city) = address;
    IO.println("Street is "+street);
    IO.println("City is "+city);
    // do something more useful
}

Möglicherweise ließe sich Pattern‑Dekonstruktion auch auf Factories anwenden (siehe Where is the Java Language Going), wodurch wir diese ausführliche Dekonstruktion:

void main(){
    // construction
    Optional<Shape> maybeShape = Optional.of(Ball.of(RED, 1));
    // verbose deconstruction by hand
    Shape s = maybeShape.orElse(null);
    if(s != null && s.isBall() && (s.color() == RED)){
        var ball = (Ball) s;
        IO.printLn(ball.size() + " red balls");
    }
}

in den folgenden kompakten und klaren Code verwandeln könnten:

void main(){
    // construction
    Optional<Shape> maybeShape = Optional.of(Ball.of(RED, 1));
    // compact matcher
    if(maybeShape instanceOf     Optional.of(Ball.of(RED, var count))){
        IO.printLn(count + " red balls");
    }
}

Dinge auseinanderzunehmen sieht jetzt genauso aus wie sie zusammenzusetzen.

Wann wir diese Patterns bekommen, ist natürlich offen. Soweit ich weiß, ist derzeit die größte Hürde, herauszufinden, wie Dekonstruktions‑Pattern mit Klassen samt all ihrer Randfälle funktionieren. Arbeiten mit Klassen sollte sich genauso anfühlen wie Arbeiten mit Records. Sobald das geklärt ist, bekommen wir wahrscheinlich etwas Kleines namens „Derived Creation“.

Derived Creation durch Dekonstruktion

Records sind unveränderliche Objekte. Um sie zu verändern, müssen wir aktuell sogenannte Wither‑Methoden von Hand schreiben. Das ist etwas mühsam:

record Address(String street, String city){
    Address {  
        notBlank(street);  
        notBlank(city);  
    }  

    Address withStreet(String street){
        return new Address(street, this.city);
    }

    Address withCity(String city){
        return new Address(this.street, city);
    }
}


void main() {
    var newAddress = anAddress
        .withStreet("Cool Blvd")
        .withCity("Cool City");
}

Derived Record Creation ist ein JDK-Candidate‑Feature, das den obigen Code auf Folgendes verkürzen könnte:

record Address(String street, String city){
    Address {  
        notBlank(street);  
        notBlank(city);  
    }  
    // all handwritten withers now gone
}

void handle(Address anAddress) {
    var newAddress = anAddress with { // strawman syntax
        street = "Cool Blvd";
        city = "Cool City";
    }
}

Im Hintergrund zerlegt der sogenannte „Wither“ die Adresse und setzt sie mit den geänderten Werten neu zusammen, wobei immer der Konstruktor und alle darin enthaltenen Prüfungen durchlaufen werden. Java kann Records dekonstruieren, weil ihr kompletter Zustand öffentlich ist, und sie rekonstruieren, weil sie einen einzigen Konstruktor haben: den kanonischen Konstruktor.

Wenn wir Klassen ein Dekonstruktions‑Pattern und einen kanonischen Konstruktor geben, könnten wir auch Klassen „withern“, was so aussehen könnte (Strohmann‑Syntax, ein jüngerer Alternativvorschlag sind Carrier Classes):

class Address(String street, String city) { // possible canonical constructor

    private final String street = street; // possible assignment
    private final String city = city;  

    Address { // possible compact canonical constructor
        notBlank(street);  
        notBlank(city);  
    }  

    // deconstruction pattern
    pattern Address(int street, int city) {
        street = this.street;
        city = this.city;
    } 
}  

void handle(Address anAddress) {
    var newAddress = anAddress with {
        street = "Cool Blvd";
        city = "Cool City";
    }
}

Wieder sehr coole Sachen. Das JDK‑Team — genauer: das Project‑Amber‑Team — arbeitet intensiv daran, all das Realität werden zu lassen. Derived Record Creation ist eines der wahrscheinlichsten nächsten Amber‑Features. Die Blockade war bislang, das Ganze mit Klassen zum Laufen zu bringen. Es wird ein Design benötigt, das allgemein genug ist, um gleichermaßen für Klassen und Records zu funktionieren. Andernfalls hätten wir eine Methode für Records und eine andere für Klassen.

Wither, Patterns und Serialisierung nur Records zur Verfügung zu stellen, ist keine Option, weil Records sehr enge Einschränkungen mit sich bringen. Wenn ein Teil der Daten nicht unveränderlich ist, wenn man API und interne Repräsentation nicht koppeln kann oder nur ein Teil des Zustands sichtbar sein soll, muss man eine Klasse verwenden. Sobald man das tut, ist „die Benutzererfahrung wie ein Sturz von einer Klippe. Schon eine kleine Abweichung vom Record‑Ideal bedeutet, dass man wieder bei Null anfangen und explizite Konstruktor‑Deklarationen, Accessor‑Methoden und Object‑Methoden schreiben und auf Dekonstruktion via Pattern Matching verzichten muss“.

Die Zukunft

The best way to predict the future is to invent it.
Alan Kay

Destructoring, die Methode, ein Objekt zu dekonstruieren, ist die Zukunft von Javas Encapsulation. Die obigen Beispiele zeigen, wie viel sicherer und lesbarer Code wird, sobald Dekonstruktion in Java ein First‑Class Citizen ist und Rekonstruktion immer über den Konstruktor läuft. Sie ermöglicht einfache und fortgeschrittene Anwendungsfälle wie Serialisierung, Klassen‑Patterns, Factory‑Patterns und natürlich Derived Record‑ wie auch Class‑Creation. Alles großartige Features. Die einzige offene Frage lautet nun: Destructoring wann?

Die Entfernung der Legacy‑Serialization‑API ist seit 2018 geplant, eine grobe Idee existiert seit 2019, und an Dekonstruktion wird etwa seit 2024 gearbeitet. Der nächste JDK‑Release wird im Juni 2026 vom Mainline‑Branch abgezweigt, mit General Availability (GA) im September 2026. Der darauffolgende ist für GA im März 2027 vorgesehen. Heißt das wir könnten Destructoring bereits 2027 bekommen? Wir werden sehen.

In der Zwischenzeit können Sie sich die begleitenden Codebeispiele ansehen und lernen, wie man Jackson ohne No‑Args‑Konstruktor und ohne Annotationen verwendet. Darüber hinaus gibt es großartige Artikel und Videos vom JDK‑Team:

Total
0
Shares
Previous Post

Vaadin Security, Teil 3.3 – Vaadin als REST-Client, drei Anbindungsstile und Bilanz

Next Post

Vaadin Security, Teil 4.1 – Bootstrap des ersten Administrators: Anwender-Sicht und der adapter-neutrale Kern

Related Posts