Record Patterns aus Java 21 (Preview in Java 19) sind zwar nicht mehr der heißeste Scheiß, da ich jetzt aber als Enterprise-Entwickler mit Java 11 mein Geld verdiene, immer noch neu genug, um ein bisschen darüber zu reden.

Erstmal also…

Was machen Sachen? 1

Wenn Records einem (deconstruction) Pattern über den Weg laufen, passiert ihnen dasselbe, wie mir damals regelmäßig in Overwatch Ranked. Sie werden absolut auseinander genommen.

Stellen wir uns vor, wir haben eine Methode, welche ein Tier erhält. Wenn es ein Haustier ist, werden Name und Alter ausgegeben, wenn es ein wildes Tier ist, die Art und das Alter.

Der altertümliche Approach (vor JDK 14) wäre:

void printAnimal(Animal animal) {
  // Natürlich sind Pet und WildAnimal hier normale Klassen
  if(animal instanceof Pet) {
    Pet pet = (Pet) animal;
    System.out.println(pet.name() + " " + pet.age()); 
  }
  if(animal instanceof WildAnimal) {
    WildAnimal wildAnimal = (WildAnimal) animal;
    System.out.println(wildAnimal.species() + " " + wildAnimal.age()); 
  }
}

Der konventionelle Approach ab Java 14 wäre:

void printAnimal(Animal animal) {
  if(animal instanceof Pet pet)
    System.out.println(pet.name() + " " + pet.age());
  if(animal instanceof WildAnimal wildAnimal)
    System.out.println(wildAnimal.species() + " " + wildAnimal.age());
}

Jetzt mit Record Patterns (ab JDK 21):

void printAnimal(Animal animal) {
  if(animal instanceof Pet(var name, var age))
    System.out.println(name + " " + age);
  if(animal instanceof WildAnimal(var species, var age))
    System.out.println(species) + " " + age); 
}

und jetzt auch noch in ‘nem switch-case (ab JEP 433):

void printAnimal(Animal animal) {
  switch(animal) {
    case Pet(var name, var age) -> System.out.println(name + " " + age);
    case WildAnimal(var species, var age) -> System.out.println(species + " " + age);
  }
}

Ich glaube es ist bereits ganz gut zu erkennen, aber der Record-Pattern Teil ist der hier:

NameVomRecord(var erstesFeldImRecord, var zweitesFeldImRecord /* usw */);
  • NameVomRecord sagt, welchen Record wir auseinandernehmen wollen
  • var feldImRecord ist einfach ein kürzerer Weg, das Feld in einer Variable zu speichern, ohne nochmal den gleichnamigen getter aufzurufen
  • Pattern wie diese können nach aktuellem Stand hinter instanceof und in switch-case-statements und -expressions stehen
  • Ungenutzte Felder können auch mit dem in Java 9 geklauten Unterstrich _ vermerkt werden

…aber ich schweife schon wieder ab; der Sinn dieses Blogposts ist es (wie eigentlich immer) nicht, ein Tutorial zu sein, sondern ein Rant.

Zeit für den Rant

Im Zusammenhang mit Record Patterns stellen sich mir einige Fragen, wie…

Warum geht das in Schleifen nicht mehr?

For-Loops konnten mal für eine kurze Zeit so aussehen:

for(Entry(var key, var value) : customMapImpl)
    System.out.print(key + " = " + value);

Das wurde wieder abgeschafft! Frech oder?

Warum können Methoden das nicht?

Wäre es nicht cool zu sagen, eine Methode bekommt einen Record aber man nimmt ihn direkt auseinander?

void printPet(Pet(var name, var age)) {
  System.out.println(name + " " + age);
}

Okay. Dumme Frage. Natürlich wäre es das!

Warum können Lambdas das nicht?

Lambdas sind Methoden und das hier ist die selbe Frage wie die davor? Interessiert mich nicht. Ich will das hier schreiben können:

(Position(x, y), direction) -> switch(direction) {
  case NORTH -> new Position(x, y + 1);
  case SOUTH -> new Position(x, y - 1);
  case EAST -> new Position(x + 1, y);
  case WEST -> new Position(x - 1, y);
};

Ach komm, wieso nicht überall?

Wieso kann man nicht gleich auch einfach ein einsames Pattern als Statement quer in die Landschaft setzen? Wie wärs mit:

void loginUser(User user, Password password) {
  if(user.loggedIn()) return;
  user -> (var name, var email, _, _);
  if(user.login(password))
    System.out.println(name + " logged in successfully with " + email);
  else
    System.out.println(name + " failed to log in with " + email);
}

Warum muss man sich entscheiden?

Guck mal, ein instanceof-Pattern:

if(response instanceof HttpResponse httpResponse)

Guck mal, ein record-Pattern:

if(response instanceof HttpResponse(int code, var body))

Wieso nicht beides in einem?

if(response instanceof HttpResponse(int code, var body) httpResponse)

(das ging schon mal in Java 20)

Warum muss ich den Datentypen angeben?

Wenn var schon erlaubt ist und Java-Klassen üblicherweise AbstractFactoryModelExceptionViewerVisitorBuilderImpl heißen, warum muss ich dann die Datentypen in Record-Patterns schreiben? Das ist doch bei Lambdas auch kein Ding?!

Wie wärs also stattdessen mit:

record BinaryTreeNode<V>(BinaryTreeNode left, BinaryTreeNode right, V value) {}

static <V> boolean search(BinaryTreeNode<V> tree, V obj) {
  return tree instanceof BinaryTreeNode(left, right, value)
    && Objects.equals(value, obj) || search(left, obj) || search(right, obj);
}

(Kannst mir jetzt nicht erzählen, dass hier drei vars sinnvoller gewesen wären)

Warum wird auf die Reihenfolge und nicht die Namen geachtet?

Records sind, im Gegensatz zu normalen Klassen, sog. “nominale Tupel”, was einfach nur heißt, dass entgegen jeder anderen Methoden oder Konstruktorsignatur, der Name (lat. nomen) der Parameter, ein Teil dieser Signatur ist. Woran merken wir das? Richtig: An den Gettern!

Records haben ja die nette Eigenschaft, eine unsichtbare Methode für jeden Konstruktorparameter zu generieren. Hierbei wird der Parametername übernommen und als Methodenname verwendet.

Angenommen ich habe diesen Record:

record Point(int x, int y) {}

und ich nutze den automatisch generierten Getter um an x zu kommen:

void someMethod(Point point) {
  point.x();
}

…ändere dann aber x im Konstruktor zu blubb, dann bekomme ich einen Compilerfehler, weil point.x() in meiner Methode nicht mehr gefunden wird. Wenn ich also an einem großen Projekt oder einer öffentlichen Bibliothek schreibe, heißt das, dass ich meine Felder im Record nicht mehr umbenennen kann, da sonst Code der diese aufruft, an anderen Stellen bricht. Das ist von mir aus auch okay, aber warum zum Teufel verwenden wir dann nicht (wie bei den Gettern) die Namen der Recordfelder aus der Deklaration?

record Point(int x, int y) {}

if(new Point(10, 20) instanceof Point(var blubb, var blip)) // <- Das hier geht

Würde man hier die Einschränkung übertragen, dass ein Record im Pattern nur über die notwendigen Getter-Namen dekonstruiert wird, dann wären sowohl Reihenfolge, Typisierung als auch Menge der Felder egal. Und vorallem das Letzte spielt in meinen Augen eine wesentliche Rolle!

Ab Java 21 ist es nämlich auch so, dass wenn ich einen Record implementiere, ich effektiv keine weiteren Felder mehr hinzufügen darf. Es könnte ja jemand anderes ein Record-Pattern nutzen, und der müsste dann ebenfalls nachziehen:

record Point(int x, int y, int x) {} // Jetzt auch mit x

if(new Point(10, 20) instanceof Point(var x, var y)) // ❗️ Compilerfehler: Da fehlt ein Unterstrich!

Würde man nur auf die Namen zugreifen, hätte man dieses Problem nicht und auch das Theater mit dem Unterstrich wäre einem erspart geblieben.

// Kompatibel, wenn nach den Namen im Record-Konstruktor gesucht wird
record Point(int x, int y, int x) {}
if(new Point(10, 20) instanceof Point(y, x))

Fazit

Normalerweise bin ich immer wieder stark davon beeindruckt, wie kompetent die JDK-Entwickler sind. Sie schreiben über Jahrzehnte tausende Prototypen nur um ein Feature perfekt zu machen. Ihr Anspruch ist absolute Perfektion, denn ihre Änderungen an der Sprache können sie fast unmöglich wieder Rückgängig machen (zumindest versprechen sie das). Das finde ich krass, und es ist auch ein großer Teil davon, warum ich die Sprache und das Ökosystem drumherum so liebe.

Bei Record-Patterns bin ich mir jedoch unsicher. Ich will keinen riesigen Deklarationsblock im Kopf meines if-Statements, schon gar nicht wenn das Wort davor instanceof und nicht einfach is ist. Der Gedanke so etwas dann auch noch zu verschachteln? Ihh! Das funktioniert schon in Beispielen nicht allzu gut; wir haben schließlich nicht alle einen Ultra-Wide Monitor.

Besonders beim letzten Punkt, der Reihenfolge und Bennenung der Felder eines Patterns habe ich einfach keine gute Antwort gefunden, und das stört mich, weil der Ist-Zustand einfach so un-Java-like ist.

Wenn du zu diesem Problem irgendwo eine(n) Kommentar / Blogpost / Vortrag / Mail / Statement der Entwickler gefunden hast, schick ihn mir gerne, bis dahin, frohes Dekonstruieren!