Record-Klassen

TL;DR

Häufig schreibt man relativ viel Boiler Plate Code, um einfach ein paar Daten plus den Konstruktor und die Zugriffsmethoden zu kapseln. Und selbst wenn die IDE dies zum Teil abnehmen kann - lesen muss man diesen Overhead trotzdem noch.

Für den Fall von Klassen mit final Attributen wurden in Java14 die Record-Klassen eingeführt. Statt dem Schlüsselwort class wird das neue Schlüsselwort record verwendet. Nach dem Klassennamen kommen in runden Klammern die "Komponenten" - eine Auflistung der Parameter für den Standardkonstruktor (Typ, Name). Daraus wird automatisch ein "kanonischer Konstruktor" mit exakt diesen Parametern generiert. Es werden zusätzlich private final Attribute generiert für jede Komponente, und diese werden durch den kanonischen Konstruktor gesetzt. Außerdem wird für jedes Attribut automatisch ein Getter mit dem Namen des Attributs generiert (also ohne den Präfix "get").

Beispiel:

public record StudiR(String name, int credits) {}

Der Konstruktor und die Getter können überschrieben werden, es können auch eigene Methoden definiert werden (eigene Konstruktoren müssen den kanonischen Konstruktor aufrufen). Es gibt außer den über die Komponenten definierten Attribute keine weiteren Attribute. Da eine Record-Klasse intern von java.lang.Record ableitet, kann eine Record-Klasse nicht von weiteren Klassen ableiten (erben). Man kann aber beliebig viele Interfaces implementieren. Record-Klassen sind implizit final, d.h. man nicht von Record-Klassen erben.

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Record-Klassen sind final
  • (K2) Record-Klassen haben einen kanonischen Konstruktor
  • (K2) Die Attribute von Record-Klassen sind final und werden automatisch angelegt und über den Konstruktor gesetzt
  • (K2) Die Getter in Record-Klassen haben die Namen und Typen der Komponenten, also keinen Präfix 'get'
  • (K2) Der kanonische Konstruktor kann ergänzt werden
  • (K2) Es können weitere Methoden definiert werden
  • (K2) Record-Klassen können nicht von anderen Klassen erben, können aber Interfaces implementieren
  • (K3) Einsatz von Record-Klassen

Motivation; Klasse Studi

public class Studi {
    private final String name;
    private final int credits;

    public Studi(String name, int credits) {
        this.name = name;
        this.credits = credits;
    }

    public String getName() {
        return name;
    }

    public int getCredits() {
        return credits;
    }
}

Klasse Studi als Record

public record StudiR(String name, int credits) {}
  • Immutable Klasse mit Feldern String name und int credits => "(String name, int credits)" werden "Komponenten" des Records genannt

  • Standardkonstruktor setzt diese Felder ("Kanonischer Konstruktor")

  • Getter für beide Felder:

    public String name() { return this.name; }
    public int credits() { return this.credits; }

Record-Klassen wurden in Java14 eingeführt und werden immer wieder in neuen Releases erweitert/ergänzt.

Der kanonische Konstruktor hat das Aussehen wie die Record-Deklaration, im Beispiel also public StudiR(String name, int credits). Dabei werden die Komponenten über eine Kopie der Werte initialisiert.

Für die Komponenten werden automatisch private Attribute mit dem selben Namen angelegt.

Für die Komponenten werden automatisch Getter angelegt. Achtung: Die Namen entsprechen denen der Komponenten, es fehlt also der übliche "get"-Präfix!

Eigenschaften und Einschränkungen von Record-Klassen

  • Records erweitern implizit die Klasse java.lang.Record: Keine andere Klassen mehr erweiterbar! (Interfaces kein Problem)

  • Record-Klassen sind implizit final

  • Keine weiteren (Instanz-) Attribute definierbar (nur die Komponenten)

  • Keine Setter definierbar für die Komponenten: Attribute sind final

  • Statische Attribute mit Initialisierung erlaubt

Records: Prüfungen im Konstruktor

Der Konstruktor ist erweiterbar:

public record StudiS(String name, int credits) {
    public StudiS(String name, int credits) {
        if (name == null) { throw new IllegalArgumentException("Name cannot be null!"); }
        else { this.name = name; }

        if (credits < 0) { this.credits = 0; }
        else { this.credits = credits; }
    }
}

In dieser Form muss man die Attribute selbst setzen.

Alternativ kann man die "kompakte" Form nutzen:

public record StudiT(String name, int credits) {
    public StudiT {
        if (name == null) { throw new IllegalArgumentException("Name cannot be null!"); }

        if (credits < 0) { credits = 0; }
    }
}

In der kompakten Form kann man nur die Werte der Parameter des Konstruktors ändern. Das Setzen der Attribute ergänzt der Compiler nach dem eigenen Code.

Es sind weitere Konstruktoren definierbar, diese müssen den kanonischen Konstruktor aufrufen:

public StudiT() {
    this("", 42);
}

Getter und Methoden

Getter werden vom Compiler automatisch generiert. Dabei entsprechen die Methoden-Namen den Namen der Attribute:

public record StudiR(String name, int credits) {}

public static void main(String... args) {
    StudiR r = new StudiR("Sabine", 75);

    int x = r.credits();
    String y = r.name();
}

Getter überschreibbar und man kann weitere Methoden definieren:

public record StudiT(String name, int credits) {
    public int credits() { return credits + 42; }
    public void wuppie() { System.out.println("WUPPIE"); }
}

Die Komponenten/Attribute sind aber final und können nicht über Methoden geändert werden!

Beispiel aus den Challenges

In den Challenges zum Thema Optional gibt es die Klasse Katze in den Vorgaben.

Die Katze wurde zunächst "klassisch" modelliert: Es gibt drei Eigenschaften name, gewichtund lieblingsBox. Ein Konstruktor setzt diese Felder und es gibt drei Getter für die einzelnen Eigenschaften. Das braucht 18 Zeilen Code (ohne Kommentare Leerzeilen). Zudem erzeugt der Boilerplate-Code relativ viel "visual noise", so dass der eigentliche Kern der Klasse schwerer zu erkennen ist.

In einem Refactoring wurde diese Klasse durch eine äquivalente Record-Klasse ersetzt, die nur noch 2 Zeilen Code (je nach Code-Style auch nur 1 Zeile) benötigt. Gleichzeitig wurde die Les- und Wartbarkeit deutlich verbessert.

Wrap-Up

  • Records sind immutable Klassen:
    • final Attribute (entsprechend den Komponenten)
    • Kanonischer Konstruktor
    • Automatische Getter (Namen wie Komponenten)
  • Konstruktoren und Methoden können ergänzt/überschrieben werden
  • Keine Vererbung von Klassen möglich (kein extends)

Schöne Doku: "Using Record to Model Immutable Data".

Challenges

Betrachen Sie den folgenden Code:

public interface Person {
    String getName();
    Date getBirthday();
}

public class Student implements Person {
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy");

    private final String name;
    private final Date birthday;

    public Student(String name, String birthday) throws ParseException {
        this.name = name;
        this.birthday = DATE_FORMAT.parse(birthday);
    }

    public String getName() { return name; }
    public Date getBirthday() { return birthday; }
}

Schreiben Sie die Klasse Student in eine Record-Klasse um. Was müssen Sie zusätzlich noch tun, damit die aktuelle API erhalten bleibt?

Quellen
  • [LernJava] Learn Java
    Oracle Corporation, 2022.
    Tutorials \> Using Record to Model Immutable Data