Java Records

In dieser Folge der Java Perlen wollen wir uns die Java Records anschauen, die mit Java 14 als Preview Feature dazu gekommen sind. Preview heisst: es ist ein neues Feature, das aber noch nicht “final” ist. Die Java Community hat die Möglichkeit das Feature auszuprobieren und Feedback zu geben. Dieses wird dann (eventuell) eingearbeitet … und nach einem oder mehrere Zyklen wird das Feature dann Final.

Was ist ein Record

Es ist sicherlich schon jedem Entwickler passiert, dass man eine Methode schreiben möchte, die 2 Werte zurückgeben soll. Man kann sich dann einfach behelfen, indem man eine Pair Klasse schreibt. Diese soll eigentlich nur 2 Werte speichern. Für eine saubere Implementation braucht man:

  • einen Konstruktor
  • getter Methoden
  • eine Implementation von equals() und hashCode()
  • und als Bonus noch eine toString() Implementation

Hier ein Beispiel:

static final class Pair {
    private final int x;
    private final int y;
    public Pair(int x, int y) { this.x = x; this.y = y; }
    public int getX() { return x; }
    public int getY() { return y; }
    public boolean equals(Object o) { ... }
    public int hashCode() { ... }
    public String toString() { ... }
}

Und dann sind wir schnell bei 50 Zeilen Java Code, nur um 2 Werte zu speichern. Sicherlich lassen wir diesen Code oft durch die IDE oder Libraries erstellen. Aber pflegen muss man den Code dennoch … wenn z.B. ein neuer Wert dazu kommt, muss man Konstruktor, equals(), hashCode() und toString() anpassen.

Da kommen nun die Java Records in Spiel. Spezifiziert sind diese im JEP 359. Es sind einfache Daten Container. Die Klasse Pair kann man dann auf einen Einzeiler umschreiben. Einfach nur die Felder deklarieren und statt class nun record schreiben:

record Pair(int x, int y) { }

Verwenden kann man Records wie normale Klassen:

Pair p = new Pair(1, 2);
System.out.println(p);
System.out.println(p.x());
System.out.println(p.y());

Und erhält folgenden Output:

$ Pair[x=1, y=2]
1
2

Und man bekommt eine Menge dazu geschenkt:

  • der Container ist immutable. Die “Klasse” und alle Felder sind final
  • der Konstruktor ist implementiert
  • es gibt für jedes Feld einen getter, aber ohne den „get“ prefix
  • equals(), hashCode() und toString() sind implementiert

Wichtig: da es ein Preview Feature ist, musst man beim compilieren “—enable-preview” und als Source “-source 14” angeben. Auch zum ausführen von Java Code mit Records muss man “—enable-preview” angeben:

$ javac --enable-preview -source 14 Pair.java
$ java --enable-preview Pair

Mit Hilfe von javap kann man anschauen, was da genau für ByteCode generiert wurde.

$ javap Pair.class
Compiled from "Pair.java"
public final class Pair extends java.lang.Record {
    public Pair(int, int);
    public java.lang.String toString();
    public final int hashCode();
    public final boolean equals(java.lang.Object);
    public int x();
    public int y();
}

Wie man sieht ist Pair eine Klasse die von Record abgeleitet ist und all das Versprochene implementiert. Übrigens: selbst kann man nicht von Record ableiten.

Customise Records

Aber das war erst der Anfang. Man kann die Records je nach Fall auch noch anpassen. Möglich sind:

  • selbst einen Konstruktor implementieren. Mann kann dort z.B. den Input validieren und im Fehlerfall eine Exception werfen
  • zusätzliche Methoden definieren
  • statische Felder hinzufügen
  • ein Interface implementieren

Wir möchten uns zum Abschluss ein Beispiel ansehen, wo wir Punkte (als X, Y Koordinaten) in einer Liste haben, und diese sortiert (nach X und dann noch nach Y Koordinate) ausgeben möchten.

Wir definieren einen Record Point und implementieren das Interface Comparable. Damit es etwas kompakter aussieht, überschreiben wir noch die die Default-Implementation von toString().

record Point(int x, int y) implements Comparable {
    public String toString() {
        return String.format("<%d,%d>", x, y);
    }

    public int compareTo(Object o) {
        Point other = (Point) o;
        if (x < other.x()) return -1;
        if (x > other.x()) return 1;
        if (y < other.y()) return -1;
        if (y > other.y()) return 1;
        return 0;
    }
}

Als Test definieren wir eine Liste mit Punkten, sortieren dieses und geben sie als String
aus:

import java.util.List;
import java.util.stream.Collectors;

List <Point> points = List.of(
    new Point(1, 2),  new Point(12, 22),  new Point(8, 12),
    new Point(8, 5),  new Point(18, 21),  new Point(18, 0));

String s = points.stream()
                 .sorted()
                 .map(p -> p.toString())
                 .collect(Collectors.joining(", “));
System.out.println(s);

Und wie erwartet, sind die Punkte sortiert:

$ <1,2>, <8,5>, <8,12>, <12,22>, <18,0>, <18,21>

Referenzen

Kommentare sind geschlossen.