Java 17: Records

Aus MimiPedia

Records

Der record ist eine neue Java-Komponente die auf einer Ebene liegt mit Klassen und Interfaces. Man kann ihn beschreiben als eine nicht ableitbare Klasse unveränderlicher (immutable) Objekte.

Das schöne am record ist, daß er eine ganze Reihe von default-Implementierungen bietet, ohne daß man den ganzen boiler plate code dazu tippen muß. Die Deklaration erfolgt nicht mit dem Keyword class, sondern mit dem context keyword record.

Hier ist eine ganz einfache Deklaration:

public record Kilometer(double dist) {};

Was bietet uns der record Kilometer?

  • Ein Kilometer-Objekt hat ein einziges Feld dist from Typ double
  • Objekte werden erzeugt über new Kilometer(42.195)
  • Auf den Wert wird zugegriffen über den Getter dist()
  • Die Methode toString() liefert etwas wie "Kilometer [dist=42.195]"
  • Die Methoden equals und hashCode werden -- gemäß Kontrakt implementiert

All' das erzeugt der Compiler automatisch. Natürlich ist die Erzeugungung von records mit beliebig vielen Feldern möglich, weiter unten ist eine Beispiel mit zwei Feldern geschrieben.

Was kann man nun mit dem record anstellen?

Neben der offensichtlichen Möglichkeit, damit Data-Container ohne großen Overhead zu generieren, Kann man damit auch eigene Datentypen definieren. Denn ein record kann -- wie jede andere Klasse auch -- statische und nicht-statische Methoden enthalten. Das Einzige was nicht erlaubt ist, ist die Definition von nicht-statischen Feldern. Mit anderen Worten:

Der Status eines record-Objektes wird durch die initiale Befüllung der implizit definierten Felder bestimmt und ändert sich niemals.

Nur syntaktischer Zucker?

Records lassen uns also eine ganze Menge Code sparen, aber strenggenommen können wir damit nicht mehr machen als mit klassischen Klassen bereits möglich war. Lohnt sich die Erweiterung dann überhaupt

Tatsächlich sind Records zunächst Java-Klassen, allerdings haben sie ein eigenes Flag das sie für die JVM von normalen Klassen unterscheidbar macht. Sie werden von der JVM anders behandelt, sind effizienter und können beim Optimieren der JVM noch besser gemacht werden.

Komplexe Zahlen

Ein ganz hübsches Beispiel ist die Definition komplexer Zahlen. Die Deklaration sieht zunächst so aus:

public record Complex(double real, double img) {
 }

Der reale Anteil wird mit real bezeichnet, der imaginäre -- wir sind tippfaul -- mit img. Sie werden mit den Gettern real() und img() angefragt. Der Compiler erzeugt uns einen Konstruktor mit zwei double-Werten die der Reihenfolge der Deklaration entsprechen:

public Complex(double real, double img) {
     this.real = real;
     this.img = img;
 }

Jetzt können wir Methoden implementieren um mit den Zahlen zu rechnen -- etwa die Addition:

public Complex plus(Complex b) {
        return new Complex(this.real + b.real, this.img + b.img);
    }

Da record-Objekte nicht verändert werden können, wird bei der Addition jedesmal ein neues Objekt erzeugt. Das ist typisch für immutable objects und entspricht auch der Implementierung von Klassen wie LocalDate.

Die Additions-Methode lternativ könnte man auch statisch definieren:

public static Complex plus(Complex a, Complex b) {
        return new Complex(a.real + b.real, a.img + b.img);
    }

Will man keine extra-Methode für die Ausgabe implementieren, kann man für eine hübschere Ausgabe die toString-Methode überschreiben:

@Override
    public String toString() {
        return real + " + " + img + "i";
    }

Warum ist das cool? Mit herkömmlichen Klassen ist das schon seit Java 1.1 möglich?

  1. Der Code wird sehr übersichtlich, weil Java uns eine ganze Menge abnimmt
  2. Die Objekte sind immutable, ohne daß dafür irgendwelche Maßnahmen erforderlich wären
  3. Die Java-VM kann die Verarbeitung der records in diesem Sinne optimieren

Den kompletten Code giebt's hier.

Integrierte Validierung

Um eine eMail-Adresse zu speichern verwenden die meisten Entwickler den Java-Typ String. Das ist grundsätzlich verkehrt, wenn man die die Zeichenkette "balfasel" betrachtet, denn das ist definitiv keine keine gültige eMail-Adresse.

Weil ein String immer gegene einen beliebigen anderen String ausgetauscht werden kann, muß der String vor jeder Verwednung daraufhin geprüft werden ob er tatsächlich eine gültige eMail-Adresse enthält. Stellen wir uns eine Anwendung vor die ganz am Anfang eine eMail-Adresse beschafft und dann durch viele Schichten durchreicht bevor sie denn endlich irgendwann verwendet wird. Wenn sich dann tatsächlich herausstellt, daß die eMail-Adresse korrupt ist, was macht man dann mit dem Fehler?

Der objektorientierte Weg ist -- auch hier -- den String in einer eigenen Klasse zu kapseln. Dazu hat keine Entwickler lust. Niemand mag den ganzen Code schreiben, niemand mag die mögliche Performance-Einbuße in kauf nehmen.

Java 17 diesen Ausreden mit dem record ein Ende.

Folgende einfache Impementierung verdeutlicht die Idee

public record EmailAdress(String adr) {
   public EmailAdress(String adr) {
        if (!(adr == null && adr.contains("@")));
            throw new RuntimeException("not an eMail address: " + adr);
        }
        this.adr = adr;
    }
}

Genau wie der default-Konstruktor kann auch der für den record vom Compiler automatisch erzeugte Konstruktur durch einen Selbstgeschriebenen ersetzt werden. Im obigen Beispiel wird das record-Objekt nur dann erzeugt, wenn der als eMail-Adresse angebotene String eine Affenschaukel enthält.

Der Beispiel-Code ist auf GitHub zu finden.