4 Punkte von GN⁺ 2023-09-18 | 1 Kommentare | Auf WhatsApp teilen
  • Java 21, veröffentlicht am 19. September 2023, bringt mit record patterns und switch pattern matching funktionale Musterausdrücke nach Java, die näher an Kotlin, Rust und C# liegen
  • Durch die aufeinander aufbauenden Änderungen aus Java 14 mit switch expressions, Java 16 mit records und instanceof pattern matching sowie Java 17 mit sealed classes ist in Java 21 die Grundlage entstanden, um algebraische Datentypen zu behandeln
  • records ermöglichen durch Einschränkungen wie final, unveränderliche Referenzen und standardisierte Getter eine stabile Zerlegung von Daten; record patterns erlauben es, verschachtelte Daten direkt in switch zu entpacken
  • sealed classes/interfaces öffnen nur erlaubte Untertypen und ermöglichen damit Modelle, die einem sum type nahekommen; zusammen mit sealed interface und record lassen sich Varianten wie RGB, CMYK, YUV und HSL begrenzen
  • Das switch in Java 21 unterstützt auch null-Cases und when-Guards, aber fehlerhafte record-Accessoren oder Ausnahmen bei der Guard-Ausführung können zu java.lang.MatchException führen

Pattern Matching in Java 21 wird stabil

  • Java 21 wurde am 19. September 2023 veröffentlicht und unterstützt record patterns in switch-Blöcken und switch-Ausdrücken
  • Diese Syntax gilt als Wendepunkt, weil sich damit auch in Java funktionale Programmiermuster ähnlich wie in Kotlin, Rust und C# ausdrücken lassen
  • Die wichtigen Syntaxänderungen der letzten Java-Versionen münden in das Pattern Matching von Java 21
    • Java 14: switch expressions werden stabil
    • Java 16: records und instanceof pattern matching werden stabil
    • Java 17: sealed classes werden stabil
    • Java 21: record patterns und switch pattern matching werden stabil
  • Mit diesem Bündel an Änderungen kann Java nun algebraische Datentypen (algebraic data types) und deren idiomatische Verwendung behandeln, die sich früher nur schwer ausdrücken ließen

Minimale Konzepte aus der Typentheorie

  • Um die Funktionen von Java 21 zu verstehen, braucht man einige Konzepte aus der Typentheorie
  • Der bottom/empty type beschreibt die Menge von Werten, die nicht berechnet werden können; in gängigen Programmiersprachen ist das meist die leere Menge
    • Kotlin Nothing hat einen privaten Konstruktor, daher kann keine Instanz existieren
    • Javas Void hat ebenfalls einen privaten Konstruktor, kann aber null enthalten und ist deshalb schwer als echter bottom type einzuordnen
    • Das primitive void in Java kann nicht als Variablentyp verwendet werden und verhält sich in diesem Punkt näher daran
  • Der top type ist die universelle Menge aller Werte aller Typen
    • In Kotlin übernimmt Any diese Rolle
    • Javas Object ist schwer im selben Sinn wie der top type anderer Sprachen zu sehen, weil primitive Typen vom Objektmodell getrennt sind
  • Der unit type ist ein Typ mit genau einem Wert
    • Javas void kann bei Methodenrückgaben wie ein unit type behandelt werden, lässt sich aber nicht als Parametertyp übergeben
    • Kotlin Unit ist als object definiert und kann auch als Methodenparameter verwendet werden
  • Der boolean type hat die zwei Werte true und false und könnte auch als nullable unit type dargestellt werden, was aber unpraktisch ist

Product Type und Java records

  • Ein product type ist ein Typ, der zwei oder mehr Komponententypen bündelt; die Anzahl der Komponententypen ist seine Arity oder sein Degree
  • Das struct in C ist ein Beispiel für einen product type
    • Komponententypen wie int, char *, double, int können sich wiederholen
    • Wiederholte Typen lassen sich zusammen mit Feldnamen als geordnete Paare unterscheiden
  • Auch Tuple in Python oder Rust lassen sich als product type verstehen; dort übernimmt der Index die Rolle des Namens der Komponenten
  • Die mit Java 16 stabilisierte record class ist ein gutes Beispiel für einen product type
    • Die Felder eines record sind final, und ein record kann nicht vererbt werden
    • Der Zustand eines record wird bei der Erzeugung festgelegt und danach beibehalten
    • Werden innerhalb eines record mutable Datentypen verwendet, ist deren inhaltliche Unveränderlichkeit dadurch allerdings nicht garantiert
  • Bei normalen Java-Klassen können sich public/private-Zustand, durch Vererbung entstehender versteckter Zustand, mutable/static-Felder und nicht standardisierte Getter mischen, weshalb sich Komponenten schwer verallgemeinern lassen
  • Durch folgende Einschränkungen garantieren records eine Struktur, auf der Sprachfeatures wie Pattern Matching stabil arbeiten können
    • Ein record ist implizit eine final class und kann nicht vererbt werden
    • Es kann keine andere Klasse außer java.lang.Record erweitern
    • record-Komponenten dürfen keinen visibility modifier haben
    • Komponentenreferenzen sind immer final und werden als unveränderlich behandelt
    • Der Standard-Getter verwendet direkt den Feldnamen; für das Feld a heißt der Getter also a()
    • Das backing field ist implizit private und wird über den Getter angesprochen

Verschachtelte Daten mit record pattern zerlegen

  • Das switch pattern in Java 21 zerlegt verschachtelte record-Daten, ohne dass instanceof-Prüfungen und explizite Casts ständig wiederholt werden müssen
  • Im Beispiel werden record A(Record inner), record B(char b) und record SomeOtherRecord() verwendet
    • Im bisherigen Ansatz wurde nach if (r instanceof A) gecastet und für den inneren Wert erneut instanceof plus Cast wiederholt
    • Mit switch pattern lässt sich ein verschachtelter Wert direkt extrahieren, etwa mit case A(B(char a)) -> String.valueOf(a)
  • Ein switch-Block ist strukturell klarer als eine if-else-Kaskade und eignet sich gut, um tief verschachtelte Daten schnell auszulesen
  • Um das unter Java 21 direkt auszuführen, kann man den Code in main.java schreiben und folgenden Befehl verwenden
java --enable-preview --source 21 main.java

Sum Type und sealed classes/interfaces

  • Um eingeschränkte Auswahlmöglichkeiten auszudrücken, kann man in Java enums verwenden, aber Farbdarstellungen mit unterschiedlichen Datenstrukturen wie RGB, HSL, YUV und CMYK lassen sich nur umständlich allein mit enums modellieren
  • Man könnte per vererbungsbasierter Polymorphie eine abstrakte Klasse Color sowie Klassen RGB, CMYK, YUV und HSL anlegen, aber eine normale Klassenhierarchie ist offen
    • Nutzer einer Bibliothek könnten eine neue Klasse wie RYB erstellen und von Color erben
    • Wenn die API keine Erweiterung vorsieht, können solche neuen Varianten Abstürze oder subtile Bugs in weit entferntem Code verursachen
  • sealed classes dienen dazu, das Konzept des sum type in Java auszudrücken
    • Ein sum type ist ein Typ, der zu einem Zeitpunkt genau eine seiner Komponentenvarianten sein kann
    • Er wird auch als tagged union type bezeichnet
  • Mit dem Modifier sealed und der permits-Klausel kann man festlegen, welche Klassen erben dürfen
public sealed class Color permits RGB, CMYK, YUV, HSL {
}
  • In einer sealed-Klassenhierarchie muss jeder direkte oder indirekte Untertyp entweder sealed, non-sealed oder final sein; fehlt das, entsteht ein Compile-Fehler
    • sealed: Nur in permits genannte Typen dürfen erben
    • non-sealed: Vererbung ist wie bei einer normalen Klasse möglich
    • final: Blatt im Vererbungsbaum, also nicht weiter erweiterbar

sealed interface zusammen mit record verwenden

  • Das Destructuring beim switch pattern matching funktioniert bei records, records können jedoch keine Klasse außer Record erweitern
  • Die Lösung ist die Verwendung eines sealed interface
    • Ein sealed interface funktioniert ähnlich wie eine sealed class
    • Auch records und enums können ein sealed interface implementieren
  • Im Beispiel wird Color als sealed interface definiert und RGB, CMYK, YUV sowie HSL werden als records implementiert
public sealed interface Color permits RGB, CMYK, YUV, HSL {
    String getDescription();
}

record RGB(int red, int green, int blue) implements Color {
    public String getDescription() {
        return "RGB Color: (" + red + ", " + green + ", " + blue + ")";
    }
}
  • Danach lassen sich in switch die Werte der einzelnen records direkt extrahieren
switch (color) {
  case RGB(int red, int green, int blue) -> {
  }
  case CMYK(double cyan, double magenta, double yellow, double black) -> {
  }
  case YUV(int y, int u, int v) -> {
  }
  case HSL hsl -> {
    System.out.println(hsl.getDescription());
  }
  case null -> {
    System.out.println("How did color become null?!");
  }
}
  • Java 21 kann in switch-Blöcken und -Ausdrücken auch einen null-Case behandeln, sodass vor dem switch keine separate Nullprüfung nötig ist
  • Wenn Color ein sealed type ist, weiß Java, ob alle Fälle behandelt sind, sodass ein exhaustives switch auch ohne default-Case möglich ist

Guard Clause und when

  • Java 21 unterstützt guard clauses, also zusätzliche Bedingungen an switch-Armen
  • Eine guard clause wird über das Schlüsselwort when direkt in das case-Label integriert
switch (color) {
  case RGB(int red, int green, int blue) when red > 200 -> {
    System.out.println("Very red.");
  }
  case RGB rgb when rgb.green > 100 -> {
    System.out.println("Sort of green...");
  }
  case RGB rgb -> {
    System.out.println("Not that red...");
  }
}
  • Zuvor hätte man im Rumpf von case RGB(...) nochmals if (red > 200) schreiben müssen
  • Java nimmt eager das erste case, das zu true ausgewertet wird; deshalb sollten speziellere Cases zuerst und allgemeinere danach stehen
  • Hinter einem RGB-Case mit Guard ist zur Wahrung der Exhaustivität ein normales case RGB rgb nötig

Wann MatchException auftritt

  • Mit dem Pattern Matching in Java 21 ist auch java.lang.MatchException neu relevant
  • Wenn ein record-Accessor eine Ausnahme auslöst, kann das switch pattern fehlschlagen und eine MatchException werfen
record R(int i) {
    public int i() {
        return i / 0;
    }
}

static void exampleAnR(R r) {
    switch(r) {
        case R(var i): System.out.println(i);
    }
}
  • Im obigen Beispiel wirft der Accessor i() eine ArithmeticException, weshalb der switch-Block eine MatchException wirft
  • Laut JEP 441 ist ein record-Accessor, der immer eine Ausnahme wirft, ein sehr ungewöhnlicher Fall; ebenso ist es sehr selten, dass ein exhaustives pattern switch eine MatchException wirft
  • Selbst bei einem exhaustiven switch kann eine Ausnahme auftreten, wenn für den Selector keine der festgelegten Varianten passt
    • JEP 441 beschreibt bei enums den Fall, dass ein exhaustives switch nach der Kompilierung nicht mehr matcht, weil sich die enum-Klasse später geändert hat
  • Auch während der Ausführung einer guard clause kann eine MatchException entstehen
static void example(Object obj) {
    switch (obj) {
        case R r when (r.i / 0 == 1): System.out.println("It's an R!");
        default: break;
    }
}

Verbleibender Umfang

  • Durch die Kombination aus records, sealed types, switch pattern matching und guard clauses in Java 21 lassen sich Bausteine der funktionalen Programmierung auf Java-Code anwenden
  • Einige Themen, etwa wie generics mit switch patterns zusammenspielen, werden hier nicht behandelt
  • Im nächsten Beitrag sollen Eigenheiten und praktische Beispiele behandelt werden, mit denen sich die Art des Schreibens von Java-Code verbessern lässt

1 Kommentare

 
GN⁺ 2023-09-18
Meinungen auf Hacker News
  • Das größte Feature von Java 21 ist die Veröffentlichung von virtuellen Threads: https://openjdk.org/jeps/444
    Aus irgendeinem Grund fehlt das im Artikel. Wenn es ein Feature gibt, das bestehende Go-Entwickler zu Java locken könnte, dann dieses; und es könnte auch Leute überzeugen, die nebenläufige Patterns im reaktiven Stil nicht mögen.

    • Ich glaube nicht, dass bestehende Go-Entwickler zu Java zurückkehren werden. Ich habe zehn Jahre mit Java gearbeitet, bin dann zu Go gewechselt und habe nicht vor, zurückzugehen.
      Java-Anwendungen und -Bibliotheken sind wegen Vererbung, Packaging, Objektorientierung, Build-Tools usw. im Vergleich zu Go viel zu schwer nachzuvollziehen und zu verstehen.
      Go ist simpel und leicht zu verstehen, zu lesen und zu warten. Das Packaging ähnelt der Art, wie man Dateien auf einem Computer in einem einzigen Ordner organisiert, und die Werkzeuge sind in die Sprache eingebaut. Es fühlt sich auch nicht so an, als bräuchte man erst eine IDE wie IntelliJ, damit es halbwegs benutzbar wird.
      Vielleicht hat sich das inzwischen geändert, aber die meisten Java-Bibliotheken, die ich heutzutage sehe, wirken immer noch so.
    • Wegen Java 21 freue ich mich auf das nächste JRuby-Release mit virtuellen Threads. Charles Nutter hat in seinem JRuby-Vortrag im August in einer Demo gezeigt, welche Auswirkungen das auf Ruby-Fibers hat, und die sind ziemlich groß.
      Am JVM-Ökosystem und seinen Tools mag ich vieles, aber Java-Code zu schreiben reizt mich inzwischen kaum noch. JRuby bietet bis zu einem gewissen Grad das Beste aus beiden Welten.
      Der Vortrag ist hier, die Demo zu virtuellen Threads beginnt ungefähr bei Minute 45:
      https://youtu.be/pzm6I4liJlg?si=GtxQ4MThEaNDfC67
    • Ich bin mir nicht sicher, ob bestehende Golang-Entwickler wegen dieses Features zu Java wechseln werden. Was die Richtung angeht, frage ich mich aber, warum es in der Go-Community so wenige Concurrent-Container gibt und in der Java-Community so viele.
      Selbst sync.Map ist kein allgemeines ConcurrentMap wie in Java, sondern auf zwei spezielle Anwendungsfälle zugeschnitten. In Java gibt es Concurrent-Sets, Queues, Barriers, Phaser, Fork-Join-Pools und mehr. Auch mit Goroutinen wären solche Container durchaus nützlich; zumindest Fork-Join ist nicht gerade trivial zu implementieren. Überall Mutexes zu verwenden fühlt sich zu low-level an.
      Ich weiß, dass es Third-Party-Implementierungen gibt, aber Nebenläufigkeit korrekt umzusetzen ist so heikel, dass ich zögere, ein Third-Party-Paket zu übernehmen, wenn es nicht so ausgereift ist und von so vielen Nutzern und Entwicklern getragen wird wie JCTools in Java oder Google Guava.
    • Der Gegensatz zwischen Executor.newVirtualThreadPerTaskExecutor und go zeigt ziemlich gut, warum Go-Entwickler meiner Meinung nach nicht zu Java wechseln werden.
      Genauer gesagt ist es eigentlich nicht go, sondern eher try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(...) }.
    • Java 21 bietet auch strukturierte Nebenläufigkeit als Preview (https://openjdk.org/jeps/453). Sie nutzt die Implementierung virtueller Threads, sieht schon anhand der Beispiele ziemlich gut aus und reduziert viele der Schmerzen beim Umgang mit threadbasierter Nebenläufigkeit.
  • Ich finde, der Titel des Blogposts war keine gute Wahl. Der versteckte Untertitel lautet „Algebraic data types in Java“, und das beschreibt den Inhalt viel treffender. Ein besserer Titel wäre Algebraic data types in Java 21 gewesen.
    Vielleicht wegen des Titels driften viele Kommentare hier vom Thema ab. Ich hätte gern mehr über algebraische Datentypen, die Vor- und Nachteile der Java-Implementierung und technische Vergleiche mit anderen Sprachen gesehen.

    • Ich fände es gut, wenn es eine populärere Sprache mit algebraischen Datentypen gäbe, bin mir aber nicht sicher, ob ich algebraische Datentypen wirklich in Java sehen möchte.
      Bestehender Java-Code verschwindet ja nicht; ich frage mich, ob es wirklich besser wird, wenn solcher Code dann zufällig dazwischen gemischt wird.
    • Ursprünglich hatte ich den Titel so gewählt, ihn aber im letzten Moment geändert, und dadurch scheint der Artikel am Ende in eine falsche Richtung gelenkt worden zu sein.
  • Die hier beschriebene Funktion sealed classes fühlt sich irgendwie völlig falsch an.
    Die Logik ist: Wenn es ein normales Interface gibt, kann jeder eine neue Klasse erstellen, die es implementiert, und Code wie if (x instanceof Foo) { ... } else if (x instanceof Bar) { ... } bricht zur Laufzeit, sobald jemand eine neue Klasse hinzufügt. Wenn man also mit dem neuen Feature für sealed Interfaces verhindert, dass irgendjemand neue Klassen erstellt, die dieses Interface implementieren, dann bricht die if-Anweisung nicht.
    Aber hat objektorientierte Programmierung dieses Problem nicht längst bedacht und gelöst? Ich weiß, dass Objektorientierung gerade nicht mehr im Trend liegt, aber Java ist eine objektorientierte Sprache.
    Die Lösung besteht darin, dem Interface eine Methode hinzuzufügen und alle Klassen diese implementieren zu lassen. Dann ruft man die Methode auf, statt alle Varianten in einer riesigen if-/switch-Anweisung aufzuzählen.
    Dieser Ansatz ist besser, als die Erweiterbarkeit von Code zu verhindern; er ermöglicht Erweiterbarkeit sogar erst. Neue Implementierer müssen nur diese Methode implementieren, und da der Compiler das erzwingt, kann man sie auch nicht versehentlich weglassen.
    Das Beispiel mit Farbräumen im Artikel (RGB, CMYK usw.) ist ein sehr gutes Gegenbeispiel. Wenn ich Code schreibe, der Farbräume verwendet, kann es sein, dass ein Nutzer oder Kunde einen seltsamen, seltenen Farbraum braucht, an den ich nicht gedacht habe. Ich möchte keinen Code schreiben, der nur die in einer riesigen if-/switch-Anweisung aufgezählten Farbräume unterstützt und durch diese Struktur nicht erweiterbar ist.

    • Die Lösung, dem Interface eine Methode hinzuzufügen, wird problematisch, wenn man nicht im Voraus alle Operationen kennen kann, die künftig benötigt werden.
      Dieses Problem lösen sealed classes. Dafür entsteht aber ein neues Problem: „Was, wenn man mehr Erweiterungsklassen braucht und sie nicht alle im Voraus kennen kann?“ Am Ende läuft es auf die Frage hinaus, ob man beides erreichen kann.
      Dieses Problem nennt man Expression Problem [1].
      Es gibt statisch typisierte Sprachen, die das Expression Problem lösen können, und Java gehört dazu [2]. Allerdings ist die Vorgehensweise in Java dafür immer noch sehr komplex und umständlich, sodass sie kaum verwendet wird. Haskell, oder Scala, wenn man in der JVM-Welt bleiben möchte, macht das deutlich besser.
      [1] https://en.wikipedia.org/wiki/Expression_problem
      [2] https://koerbitz.me/posts/Solving-the-Expression-Problem-in-...
    • Der Ansatz mit sealed classes erlaubt ebenfalls, dass jeder Code erweitern kann, aber in einer anderen Dimension als der Ansatz mit Interface-Methoden.
      Interface-Methoden und virtuelle Aufrufe sind sehr unflexibel, wenn man statt neuer Klassen neue Operationen hinzufügen möchte. Selbst wenn man nur eine einzige neue Operation hinzufügen will, muss man zu allen Implementierungen gehen und eine neue Methode einfügen; dabei können auch Implementierungen kaputtgehen, auf die man keinen Zugriff hat. Außerdem müssen voneinander unabhängige Methoden in derselben Klasse definiert werden, was die Lesbarkeit des Codes stark verschlechtert, und virtuelle Aufrufe sind auch nicht kostenlos, sondern haben Auswirkungen auf die Performance.
      In diesem Fall skaliert eine sealed class deutlich besser. Man fügt an einer Stelle einen neuen switch hinzu, und das war’s; auch die Abwärtskompatibilität wird nicht gebrochen.
      Das ist das berühmte Expression Problem.
      https://pkolaczk.github.io/in-defense-of-switch/
    • Ich verstehe die Empfehlung, statt instanceof polymorphes Dispatching zu verwenden, und ich habe auch gesehen, wie Bob Martin ausführlich darüber spricht, aber ich stimme nicht zu.
      Für solches polymorphes Dispatching müssen Objekte mehrere Zuständigkeiten selbst behandeln.
      In einem Videospiel könnte Car .render(), .collide() und .playSound() haben. Wenn man später Dog hinzufügt, muss man nur diese drei Methoden implementieren und weder Renderer, PhysicsEngine noch SoundEngine ändern oder neu kompilieren. Auch ein anderer Programmierer kann Entitäten hinzufügen, ohne Bugs in meinen wertvollen Code einzubauen. Das sieht gut aus.
      Aber nun müssen Car und Dog alles über Grafik, Physik und Sound wissen. Und Entitäten existieren nicht isoliert. Autos und Hunde müssen in der richtigen Reihenfolge gerendert werden und können sich gegenseitig verdecken. Auch Kollisionen müssen gegenseitig geprüft werden. Wie ich in einem echten Game Jam erlebt habe, kann es sogar passieren, dass die für Sound zuständige Person in alle Objekte hineingehen muss, um Soundverhalten hinzuzufügen.
      Viel besser ist es, beim Nachdenken über Physik in Physics.collideAll() zu arbeiten und bei Bedarf mit instanceof Sonderfälle zu behandeln, und beim Nachdenken über Grafik in Graphics.renderAll() zu arbeiten.
      Ähnlich ist es auch in der alltäglichen Backend-Webentwicklung mit Java. Wenn man in einem REST-Controller entscheidet, wie Java-Objekte in HTTP-Antworten umgewandelt werden, ist es besser, alles in einer Methode zu sehen und {instanceof Forbidden} auf 403 sowie {instanceof NotFound} auf 404 zu mappen. Ich möchte nicht getCode() oder REST-spezifische Inhalte in die Java-Klassen selbst stecken.
    • Erweiterbarkeit zuzulassen ist nicht immer sinnvoll. Es gibt einen Grund, warum String final ist, und man könnte sogar argumentieren, dass final der Standard sein sollte und nur Klassen, die Subclassing erlauben, explizit als open markiert werden sollten.
      Ein klassisches Beispiel für einen Summentyp in der funktionalen Programmierung ist die Liste. Hier gibt es nur Element(T head, List tail) und Nil(). Es gibt keinen Grund, sie zu erweitern; tatsächlich kann eine Erweiterung in Kombination mit allen Funktionen, die Listen verarbeiten, zu fehlerhaftem Code führen.
      Außerdem ist das dem Pattern Matching ähnliche Visitor Pattern sehr wortreich und beruht auf einem Hack, der die Semantik normaler Methodendispatches in Java ausnutzt. In diesem Fall halte ich Pattern Matching für um ein Vielfaches lesbarer.
    • Für solche Features gibt es valide Anwendungsfälle.
      Man kann zum Beispiel an ein Sicherheits-Interface denken, das Security-Tokens validiert.
      Bei einem normalen Interface ist es leicht, es zu implementieren und dabei Tokens zu ignorieren (alles zuzulassen), Tokens abzugreifen oder eine Backdoor einzubauen. Wenn eine solche Klasse an der Stelle injiziert wird, an der die Sicherheitsprüfung stattfindet, kann die Sicherheit kompromittiert werden.
      Bei einem sealed Interface kann es keine nicht autorisierte neue Implementierung geben. Wenn man ein Objekt erhält, das behauptet, dieses Interface zu implementieren, ist garantiert, dass es eine der geprüften Implementierungen ist, die die tatsächliche Sicherheitsprüfung durchführen. Damit hat man eine ganze Klasse von Security-Bugs und Exploits eliminiert.
  • Aus der Perspektive von jemandem, der Sum Types kennt, aber mit Java-Sum-Types nicht besonders vertraut ist, ist das ein guter Artikel.
    Allerdings weiß ich nicht, ob allein Sum Types ausreichen, um Java wieder zu mögen. Die weitreichende Nullbarkeit bleibt weiterhin bestehen und taucht auch in diesem Artikel mehrfach auf.

    • Nullbarkeit ist in Java ein großes Problem, aber annotationsbasierte Nullability-Frameworks sind wirksam und im gesamten Ökosystem verbreitet. Persönlich halte ich sie fast für unverzichtbar.
      Ich bin wirklich gespannt auf https://jspecify.dev/, wo Google, Meta, Microsoft und andere versuchen, Annotationen beginnend mit @Nullable zu standardisieren.
    • Wenn Valhalla kommt, wird es auch explizite Nullbarkeit geben, also wird auch dieses Problem angegangen.
    • Außerdem ist Java noch keine ausdrucksorientierte Sprache.
  • Die Antwort des Artikels auf „Warum heißt es Product Type?“ ist nicht falsch, aber intuitiver und knapper gesagt: Die Gesamtzahl der möglichen Werte eines Product Type ist das Produkt der Anzahl möglicher Werte seiner Bestandteiltypen.
    Ersetzt man product durch sum, gilt dasselbe entsprechend.
    Interessanterweise lässt sich die Gesamtzahl eindeutiger Funktionen der Form a -> b, wenn man nur Eingabe und Ausgabe betrachtet, als Potenz berechnen. Also (Anzahl möglicher Werte von b) ^ (Anzahl möglicher Werte von a).

    • Noch intuitiver und knapper gesagt: Ein Product Type entspricht dem kartesischen Produkt von Mengen.
    • Maps und Listen sind ebenfalls weitere Beispiele für Exponentional Types. Reine Funktionen können theoretisch durch einen Map-Lookup auf vorberechnete Werte ersetzt werden, daher ist die Intuition, dass sie Funktionen entsprechen, naheliegend. In diesem Kontext ist eine Liste eine spezielle Map, deren Schlüssel ganze Zahlen sind.
      Mathematisch geschrieben: Bei einer Liste von Bool steht links die Anzahl der Elemente und rechts die Gesamtzahl der Möglichkeiten.
      0 : 1
      1 : 2
      2 : 4
      3 : 8
      4 : 16
      5 : 32
      Und so weiter.
  • Ich warte darauf, dass Project Valhalla abgeschlossen wird und Java endlich Value Types bekommt. Dann hätte es Sum Types, Value Types und Coroutinen und dürfte zu den ziemlich ordentlichen Sprachen gehören.

  • Java war ursprünglich wirklich keine schlechte Sprache.
    Das Problem waren die Leute: riesiges Overengineering, zu viele abstrakte Konzepte, die es schwer machen, eine Codebase zu verstehen, Code-Magie in Form von Annotationen wie rückwärts gerichtete GOTO-Anweisungen, und DI-Frameworks.
    Was repariert werden muss, ist nicht die Sprache, sondern das Ökosystem. Innerhalb des Java-Ökosystems braucht es eine Art „Reformations“-Bewegung.
    Einfach zu Kotlin, Clojure oder Scala zu wechseln reicht nicht.

    • In jeder Sprache kann man eine HammerFactoryFactory bauen, die HammerFactory-Objekte ausspuckt. Aber das Java-Ökosystem begünstigt und fördert solche Lösungsansätze. Bei C# sehe ich es ähnlich.
      Was Java wirklich braucht, sind freie Funktionen bzw. Funktionen mit Namespace. Manchmal braucht man keine Klasse; eine Funktion in einem Modul oder Namespace reicht. Ich verstehe nicht, warum das nicht möglich ist.
  • Der Autor erklärt, warum Records nötig sind, und führt an, dass die meisten Java-Objekte alle Felder private halten und nur über Lese- und Schreib-Accessor-Methoden darauf zugreifen lassen.
    Es gibt aber keine sprachseitig erzwungene Konvention zur Definition von Accessors: Wenn man den Getter von foo getBar nennt, funktioniert das zwar, kann aber Leute verwirren, die auf bar zugreifen wollen.
    Scala unterstützt Pattern Matching für Objekte, die eine unapply-Methode implementieren. Gilt dieser Ansatz als schädlich? Warum ist Java diesem Weg nicht gefolgt?

    • Das ist wieder eine Frage der Standardisierung. Die Standardisierung von Java ist langsam, ähnlich wie bei C++. Wenn man sich die letzte Fußnote des Record-Pattern-JEP ansieht, deutet sie an, dass etwas wie unapply in Vorbereitung sein könnte; die Hoffnung ist also noch nicht ganz verloren.
  • Java war schon immer eine großartige Sprache. Was einen zum Kotzen bringt, ist das Enterprise-artige Ökosystem. Ich habe schon erlebt, dass Dutzende Klassen und Interfaces verwendet wurden, um eine einzige Zeile Logik zu implementieren.