Java Records 2

In der letzten Folge haben wir angeschaut was Java Records sind und wie man diese selbst erweitern kann. Diesmal wollen wir uns weitere Beispiele anschauen.

Validierung

Java Records definieren automatisch einen Konstruktor, aber man kann diesen überschreiben. Dies kann z.B. nützlich sein um die Werte eines Records zu validieren. Dazu ein kleines Beispiel. Wir definieren ein Record Range mit unterer und oberer Grenze lo und hi. Wir möchten bei der Erstellung prüfen, ob lo wirklich kleiner ist als hi und im Fehlerfall eine Exception werfen. Dazu überschreiben wir den Konstruktor. Gemacht wird dies durch einen Codeblock mit dem Namen des Records und den {} Klammern. Wir prüfen die Werte lo und hi und werfen gegebenenfalls eine Exception. Dabei können wir gerade die toString() Methode benutzen, die der Record ja automatisch schon hat.

record Range(int lo, int hi) { 
    public Range {
        if (lo > hi) 
            throw new IllegalArgumentException(this.toString());        
    }
}
Bei korrekten Werten erhalten wir einen Record.
Range r1 = new Range(1, 2);
System.out.println(r1);

$ Range[lo=1, hi=2]
Und bei nicht korrekten Werten bekommen wir eine Exception.
Range r2 = new Range(2, 1);
System.out.println(r2);

$ java.lang.IllegalArgumentException: Range[lo=0, hi=0]
	at Range.<init>(#17:5)
	at .(#21:1)

Mehrere Return Werte

Dass eine Methode mehrere Werte zurückgeben soll war ja unser Ausgangspunkt in der letzten Folge der Java Perlen. Wir haben die Klasse Pair definiert und dann umgeschrieben in einen Record. Hier nun ein weiteres Beispiel zu diesem Thema, welches mehrere Features der neueren Java Versionen nutzt. Wir möchten in einer Methode aus einer Input Liste (bzw. Stream) von Integers das min und max zurückgeben.  Java 8 bietet alles was dazu nötig ist. Man kann dazu min /max von den Streams und den Comparator von Integer nutzen. Dann die beiden Werte in einen Record packen … und fertig.

Optional <Integer> min = Stream
  .of(1, 8, 2, 5)
  .min(Integer::compareTo);

Optional <Integer> max = Stream
  .of(1, 8, 2, 5)
  .max(Integer::compareTo);
Es gibt aber auch die Möglichkeit von Collectors minBy und maxBy zu nutzen.
Optional <Integer> min = Stream
    .of(1, 8, 2, 5)
    .collect(Collectors.minBy(Integer::compareTo));

Optional <Integer> max = Stream
    .of(1, 8, 2, 5)
    .collect(Collectors.maxBy(Integer::compareTo));
Beide Ansätze haben das gleiche Problem: man braucht zwei mal einen Stream mit denselben Daten und muss zwei mal den gesamten Stream verarbeiten. Funktioniert, ist aber nicht wirklich performant. Mit Java 12 wurde der neue Teeing Collector eingeführt. Dieser erlaubt die Verarbeitung der Werte eines Stream mit mehreren Collectors. Die offizielle Doku sagt dazu:“…returns a Collector that is a composite of two downstream collectors. Every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result.” In unserem Beispiel haben wir einen Stream von Integers und die beiden minBy und maxBy Collectors. Die beiden Collectors können parallel auf dem gleichen Stream arbeiten und dabei jeweils min bzw. max bestimmen. Wir erhalten zwei Optional, die wir in einem finalen Schritt in ein MinMax Record umwandeln. Im Code sieht das dann folgendermassen aus.
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

record MinMax<T>(T min, T max) {}

static <T> MinMax<T> minMax(Collection<T> elements, 
                            Comparator<T> comparator) {

    return elements
            .stream()
            .collect(Collectors.teeing(
                    Collectors.minBy(comparator),
                    Collectors.maxBy(comparator),
                    (i, j) -> new MinMax <>(i.get(), j.get())
            ));
}
Und wir erhalten performant und mit wenig Code min und max gekapselt in einem Record.
MinMax mm = minMax(List.of(1, 8, 2, 5), Integer::compareTo);
System.out.println(mm);

$ MinMax[min=1, max=8]
import java.util.Map;
import java.util.HashMap;
import java.time.LocalDateTime;

record AccountId(String accountNumber, String accountType) {}

Map<AccountId, LocalDateTime> lastLogin = new HashMap<>();

AccountId id1 = new AccountId("111-22-a", "type1");
AccountId id2 = new AccountId("111-22-a", "type2");

lastLogin.put(id1, LocalDateTime.now());
lastLogin.put(id2, LocalDateTime.now().minusDays(1));

System.out.println(lastLogin.get(id1));
System.out.println(lastLogin.get(id2));


$ 2020-10-08T09:35:58.876069
  2020-10-07T09:35:58.918702

Compound Map Keys

In der Datenbankwelt ist es oft so, dass eine Id zusammensetzt ist aus mehreren Werten. Eine Id für einen Account könnte z.B. aus einer accountNumber und einem accountType bestehen. Auch in Java kann man solche “compound keys” als keys für Maps brauchen. Und mit Records (die ja immutable sind) geht das auch sehr einfach. Wir definieren einen Record AccountId, den wir dann als Key für die Map lastLogin brauchen.

Records sind ein wirkliche cooles Java Feature, das einige Dinge im Entwickler Alltag vereinfachen kann. Wie schon eingangs erwähnt ist es ein Preview Feature. Es bleibt spannend, wie Records final aussehen werden (in Java 15 sind Records in “Second Preview”) … und wohin sich die Records in der weiteren Zukunft entwickeln werden. Wir bleiben auf jeden Fall dran.

Referenzen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.