Methoden in Records?
Zunächst könnte man denken, dass Methoden in Records dem kompakten Grundprinzip des Records widersprechen.
Tatsächlich können Records aber eigene Methoden enthalten. Diese Methoden greifen dann auf die Komponenten des Records zu oder führen Berechnungen auf deren Basis durch.
Bei unserem Record Person könnte man beispielsweise eine Altersprüfung einbauen. Dabei soll überprüft werden, ob das übergebene Alter mindestens 0 Jahre beträgt und höchstens 130 Jahre alt ist.
Um eine solche Altersverifikation durchzuführen, gibt es technisch zwei Möglichkeiten
1. Direkter Einbau in den Konstruktor
Moment mal - Records haben doch keinen Konstruktor - oder?
Das folgende Beispiel beantwortet diese Frage:
public record Person(String name, String vorname, int alter)
{
public Person
{
if (alter < 0 || alter > 130)
throw new IllegalArgumentException(
"Alter muss zwischen 0 und 130 liegen");
}
}
Hier sehen wir tatsächlich einen Konstruktor in dem Record Person. Allerdings handelt es sich nicht um den normalen Klassen-Konstruktor, sondern um einen sogenannten kompakten Konstruktor. Dieser kompakte Konstruktor besteht nur aus dem Wort public und dem Bezeichner des Records, eine Parameterliste fehlt völlig.
Innerhalb dieses Konstruktors kann aber auf die drei Komponenten name, vorname und alter zugegriffen werden, allerdings nur lesend, da diese Komponenten final sind.
In diesem Beispiel wird der kompakte Konstruktor um eine Validierung erweitert. Erst wenn die beiden Ausschluss-Bedingungen dieser Validierung nicht zutreffen, übernimmt der Compiler die Zuweisung der Parameter an die Instanzvariablen des Records. Andernfalls wird eine IllegalArgumentException geworfen.
2. Implementierung einer speziellen Methode
Alternativ kann die Prüfung in eine eigene Methode ausgelagert werden:
public record Person(String name, String vorname, int alter)
{
public Person
{
pruefeAlter(alter);
}
private void pruefeAlter(int alter)
{
if (alter < 0 || alter > 130)
throw new IllegalArgumentException(
"Alter muss zwischen 0 und 130 liegen");
}
}
Hier übernimmt die private statische Methode pruefeAlter() die Validierung des Alters. Der kompakte Konstruktor ruft diese Methode auf, bevor das Record-Objekt vollständig erzeugt wird.
Diese zweite Variante ist etwas umfangreicher, kann aber sinnvoll sein, wenn die Validierung komplexer wird oder an mehreren Stellen benötigt wird. Für einfache Prüfungen genügt meist die erste, kompaktere Lösung.
Wenn wir beispielsweise den Namen und den Vornamen überprüfen möchten, könnte man den Record um eine private Mehtode pruefeString(String s) erweitern, die dann von dem kompakten Konstruktor jeweils einmal für die Komponente name und die Komponente vorname aufgerufen wird:
public record Person(String name, String vorname, int alter)
{
public Person
{
pruefeAlter(alter);
pruefeString(name);
pruefeString(vorname);
}
private void pruefeAlter(int alter)
{
if (alter < 0 || alter > 130)
throw new IllegalArgumentException(
"Alter muss zwischen 0 und 130 liegen");
}
private void pruefeString(String s)
{
if (s == null)
throw new NullPointerException(
"Name oder Vorname ist null!");
if (s.length() < 4)
throw new IllegalArgumentException(
"Name oder Vorname ist zu kurz!");
}
}
Öffentliche Methoden in Records
Eine typische Frage, die ich von Studenten in diesem Zusammenhang immer wieder höre, ist diese:
Wieso müssen die beiden Prüfmethoden überhaupt als private deklariert werden, sind denn nicht alle Methoden in einem Record automatisch private?
Das ist eine sehr gute Frage. Tatsächlich kann man einen Record auch um öffentliche Methoden erweitern. Betrachten wir dazu folgendes Beispiel:
public record Punkt(int x, int y)
{
public double abstandVomUrsprung()
{
return Math.sqrt(x * x + y * y);
}
}
Dieser Record besitzt eine öffentliche Methode, mit der man den Abstand des Punktes vom Koordinaten-Ursprung berechnen kann. Das ist eine durchaus sinnvolle Methode, die man nach der Erzeugung eines Punkt-Objekts benutzen kann.
Solche öffentlichen Methoden eines Records können die Komponenten des Records (hier x und y) jedoch nicht verändern, sie können sie nur auswerten und Berechnungen mit ihnen anstellen.
Hier ein zweites Beispiel:
public record Person(String name, String vorname, int alter)
{
public String vollerName()
{
return vorname + " " + name;
}
public boolean istVolljaehrig()
{
return alter >= 18;
}
}
Hier haben wir zwei öffentliche Methoden. Die erste liefert den vollen Namen der Person zurück, die zweite überprüft die Volljährigkeit der Person und liefert true bzw. false zurück. Beide Methoden verändern nichts an den Werten der Komponenten - sie können das ja auch gar nicht, weil diese final sind.
Ein etwas größeres Beispiel
Betrachten wir den folgenden Record:
public record Personendaten (String name, String vorname, int alter)
{
public Personendaten
{
pruefeAlter(name, vorname, alter);
pruefeString(name);
pruefeString(vorname);
}
private void pruefeAlter(String name, String vorname, int a)
{
if (a < 10 || a > 120)
throw new IllegalArgumentException
("Alter von " + vorname + " " + name +
" ist nicht zwischen 10 und 120, sondern " + a);
}
private void pruefeString(String s)
{
if (s == null)
throw new NullPointerException
("String " + s + " ist null!");
if (s.length() < 3)
throw new IllegalArgumentException
("String ist zu kurz: " + s);
if (s.length() > 20)
throw new IllegalArgumentException
("String ist zu lang: " + s);
}
public String getName()
{
return vorname + " " + name;
}
}
Der kompakte Konstruktor ruft hier drei Validierungs-Methoden auf, die das Alter, den Namen und den Vornamen einer Person überprüfen. Die Methode zur Alters-Validierung hat sogar drei Parameter, nämlich Name, Vorname und Alter. Die Exception soll nämlich genau angeben, bei welcher Person dieser Fehler aufgetreten ist.
Die pruefeString()-Methode erhält den Namen oder den Vornamen als Parameter und führt drei Überprüfungen durch: String == null ?, String zu kurz ?, String zu lang?
Beide Validierungsmethoden sind private und daher nur innerhalb des Records aufrufbar. Die letzte Methode getName() ist dagegen public und kann auch von außen verwendet werden.
Bei der Methode pruefeAlter() taucht nun automatisch die Frage auf: Wieso müssen name und vorname als Parameter angegeben werden? Hat die Methode nicht Zugriff auf die Komponenten name und vorname?
Das ist eine gute Frage. Während der Ausführung des kompakten Record-Konstruktors sind die Komponenten (this.name, this.vorname, this.alter) noch nicht initialisiert. Erst nachdem der Konstruktor beendet ist, fügt der Compiler automatisch die Zuweisungen
this.name = name; this.vorname = vorname; this.alter = alter;
ein.
Zu dem Zeitpunkt, an dem pruefeAlter() aufgerufen wird, können die Methoden daher noch nicht auf diese Komponenten zugreifen.
Die Parameter name und alter stehen jedoch bereits zur Verfügung und können daher der Methode pruefeAlter() übergeben werden.
Records können Record-Objekte enthalten
Schauen wir uns nun einen zweiten Record an:
public record Person(Personendaten pDaten, double gewicht, double groesse)
{
public Person
{
pruefePersonendaten(pDaten);
pruefeGewicht(gewicht);
pruefeGroesse(groesse);
}
private void pruefePersonendaten(Personendaten pD)
{
if (pD == null)
throw new NullPointerException("Personendaten sind null!");
}
private void pruefeGewicht(double g)
{
if (g < 20)
throw new IllegalArgumentException
("class Person: Das Gewicht ist < 20 kg");
if (g > 130)
throw new IllegalArgumentException
("class Person: Das Gewicht ist > 130 kg");
}
private void pruefeGroesse(double g)
{
if (g < 120)
throw new IllegalArgumentException
("class Person: Die Groesse ist < 120 cm");
if (g > 220)
throw new IllegalArgumentException
("class Person: Die Groesse ist > 220 cm");
}
public double getIdealgewicht()
{
return (groesse - 100) * 0.9;
}
}
Der Konstruktor des Records Person erhält als Parameter ein Objekt pDaten des Records Personendaten. Records können also problemlos andere Records als Komponenten besitzen.
Auf die drei Validierungs-Methoden soll hier nicht näher eingegangen werden. Das Beispiel soll nur zeigen, dass sich Record-Objekte im praktischen Einsatz fast genauso verwenden lassen wie Objekte von normalen Klassen. Sie können als Komponenten anderer Records oder Klassen dienen, als Parameter übergeben und von Methoden zurückgegeben werden.
Merke:
Records mit Methoden
Obwohl Records eine besondere Form von Klassen sind, verhalten sie sich im Alltag fast genauso wie normale Klassen. Sie können eigene Methoden besitzen, andere Record- oder Klassenobjekte als Komponenten enthalten und überall dort verwendet werden, wo auch gewöhnliche Objekte eingesetzt werden.
Records sind lediglich eine spezielle, kompakte Form einer Klasse, die für unveränderliche Datenobjekte gedacht ist.