4 points par GN⁺ 2023-09-18 | 1 commentaires | Partager sur WhatsApp
  • Sorti le 19 septembre 2023, Java 21 introduit les record patterns et le pattern matching dans switch, rapprochant Java de l’expression des patterns fonctionnels qu’on trouve en Kotlin, Rust ou C#
  • Avec l’accumulation des switch expressions de Java 14, des records et du pattern matching avec instanceof de Java 16, puis des sealed classes de Java 17, Java 21 réunit enfin les bases nécessaires pour manipuler des types de données algébriques
  • Les records permettent de décomposer les données de façon fiable grâce à des contraintes comme final, des références immuables et des getters standardisés ; les record patterns permettent d’extraire directement des données imbriquées dans un switch
  • Les sealed classes/interfaces n’autorisent que certains sous-types, ce qui permet de modéliser quelque chose de proche d’un sum type ; en combinant sealed interface et record, on peut limiter les variantes comme RGB, CMYK, YUV ou HSL
  • Le switch de Java 21 prend aussi en charge le cas null et les gardes avec when, mais un mauvais record accessor ou une exception pendant l’évaluation d’une garde peut conduire à une java.lang.MatchException

Le pattern matching stabilisé dans Java 21

  • Java 21 est sorti le 19 septembre 2023 et prend en charge les record patterns dans les blocs switch et les expressions switch
  • Cette syntaxe est considérée comme un tournant, car elle permet à Java d’exprimer des patterns de programmation fonctionnelle d’une manière proche de Kotlin, Rust et C#
  • Les principaux changements syntaxiques des versions récentes de Java convergent vers le pattern matching de Java 21
    • Java 14 : stabilisation des switch expressions
    • Java 16 : stabilisation des records et du pattern matching avec instanceof
    • Java 17 : stabilisation des sealed classes
    • Java 21 : stabilisation des record patterns et du switch pattern matching
  • Avec cet ensemble d’évolutions, Java peut désormais manipuler des types de données algébriques (algebraic data types) et leurs usages idiomatiques, auparavant difficiles à exprimer

Notions minimales de théorie des types

  • Pour comprendre les fonctionnalités de Java 21, quelques notions de théorie des types sont nécessaires
  • Le bottom type ou empty type représente l’ensemble des valeurs qui ne peuvent pas être calculées ; dans les langages de programmation courants, il s’agit généralement de l’ensemble vide
    • En Kotlin, Nothing ne peut pas avoir d’instance, car son constructeur est privé
    • En Java, Void a aussi un constructeur privé, mais peut contenir null, donc il est difficile d’en faire un véritable bottom type
    • Le type primitif void de Java ne peut pas être utilisé comme type de variable, ce qui le rapproche davantage de ce comportement
  • Le top type est l’ensemble universel qui regroupe toutes les valeurs de tous les types
    • En Kotlin, ce rôle est rempli par Any
    • En Java, Object est difficile à assimiler exactement au top type d’autres langages, car les types primitifs sont séparés du modèle objet
  • Le unit type est un type qui ne possède qu’une seule valeur
    • En Java, void peut être traité comme un unit type pour les retours de méthode, mais ne peut pas être passé comme type de paramètre
    • En Kotlin, Unit est défini comme un object et peut aussi être utilisé comme type de paramètre de méthode
  • Le boolean type contient deux valeurs, true et false ; on peut aussi le représenter comme un unit type nullable, mais ce n’est pas pratique

Les product types et les records Java

  • Un product type est un type qui regroupe deux types composants ou plus ; le nombre de composants correspond à son arité ou son degré
  • Le struct du C est un exemple de product type
    • Des types composants comme int, char *, double, int peuvent se répéter
    • Quand un type se répète, on peut le distinguer en le considérant avec son nom de champ comme une paire ordonnée
  • Les tuples de Python ou de Rust peuvent aussi être vus comme des product types ; dans ce cas, l’index joue le rôle du nom des composants
  • La record class stabilisée en Java 16 est un bon exemple de product type
    • Les champs d’un record sont final et un record ne peut pas être hérité
    • L’état d’un record est fixé à la création puis conservé ensuite
    • En revanche, si un record contient un type de donnée mutable, cela ne garantit pas l’immuabilité de son contenu
  • Une classe Java classique peut mélanger état public/privé, état caché issu de l’héritage, champs mutables ou statiques, et getters non standard, ce qui rend difficile la généralisation de ses composants
  • Les records garantissent, grâce aux contraintes suivantes, une structure sur laquelle des fonctionnalités du langage comme le pattern matching peuvent fonctionner de manière fiable
    • un record est implicitement une classe final et ne peut pas être hérité
    • il ne peut pas étendre une autre classe que java.lang.Record
    • on ne peut pas ajouter de modificateur de visibilité à un composant de record
    • les références aux composants sont toujours final et traitées comme immuables
    • le getter par défaut reprend exactement le nom du champ ; le getter du champ a est donc a()
    • le champ de backing est implicitement privé et on y accède via le getter

Décomposer des données imbriquées avec les record patterns

  • Les switch patterns de Java 21 permettent de décomposer des données record imbriquées sans répéter des tests instanceof et des casts explicites
  • L’exemple utilise record A(Record inner), record B(char b) et record SomeOtherRecord()
    • Avec l’ancienne approche, il fallait faire if (r instanceof A), puis caster, puis répéter instanceof et le cast pour la valeur interne
    • Le switch pattern permet d’extraire directement une valeur imbriquée avec case A(B(char a)) -> String.valueOf(a)
  • Un bloc switch est plus lisible qu’une suite de if-else et convient bien à l’extraction rapide de données profondément imbriquées
  • Pour l’exécuter directement en Java 21, il faut placer le code dans main.java puis utiliser la commande suivante
java --enable-preview --source 21 main.java

Les sum types et les sealed classes/interfaces

  • On peut utiliser un enum Java pour exprimer un ensemble limité de choix, mais des représentations de couleur comme RGB, HSL, YUV ou CMYK, qui ont chacune une structure de données différente, sont fastidieuses à modéliser avec un enum seul
  • On peut créer une classe abstraite Color et des classes RGB, CMYK, YUV, HSL via le polymorphisme fondé sur l’héritage, mais une hiérarchie de classes classique reste ouverte
    • Un utilisateur de bibliothèque peut créer une nouvelle classe comme RYB qui hérite de Color
    • Si l’API n’était pas conçue pour être étendue, une nouvelle variante peut provoquer des crashs ou des bugs subtils dans du code éloigné
  • Les sealed classes servent à exprimer en Java un concept proche du sum type
    • Un sum type est un type qui, à un instant donné, ne peut être qu’un seul de ses composants
    • On l’appelle aussi tagged union type
  • Avec le modificateur sealed et la clause permits, on peut n’autoriser l’héritage qu’à certaines classes précises
public sealed class Color permits RGB, CMYK, YUV, HSL {
}
  • Dans une hiérarchie de sealed classes, chaque sous-classe directe ou indirecte doit être marquée sealed, non-sealed ou final, faute de quoi une erreur de compilation se produit
    • sealed : seuls les types nommés dans permits peuvent l’hériter
    • non-sealed : l’héritage redevient possible comme avec une classe classique
    • final : le type est une feuille de l’arbre d’héritage et ne peut plus être étendu

Utiliser ensemble sealed interface et record

  • La déstructuration via le switch pattern matching fonctionne avec les records, mais un record ne peut pas hériter d’une classe autre que Record
  • La solution consiste à utiliser une sealed interface
    • Une sealed interface fonctionne globalement comme une sealed class
    • Les records et les enums peuvent aussi implémenter une sealed interface
  • Dans l’exemple, Color devient une sealed interface et RGB, CMYK, YUV, HSL sont implémentés comme records
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 + ")";
    }
}
  • Il devient ensuite possible d’extraire directement les valeurs de chaque record dans un switch
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 permet de gérer le cas null dans les blocs et expressions switch, ce qui évite d’avoir à faire un test null séparé avant le switch
  • Si Color est un sealed type, Java peut savoir si tous les cas sont couverts, ce qui permet un switch exhaustif sans default

Clauses de garde et when

  • Java 21 prend en charge les clauses de garde pour ajouter des conditions supplémentaires à un bras de switch
  • Une clause de garde intègre la condition directement dans le label case via le mot-clé when
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...");
  }
}
  • Auparavant, il fallait remettre un if (red > 200) dans le corps du case RGB(...)
  • Java évalue avidement le premier case qui retourne true ; il faut donc placer d’abord les cas les plus spécifiques, puis les moins spécifiques
  • Après un case RGB avec garde, un case RGB rgb générique reste nécessaire pour conserver l’exhaustivité

Quand une MatchException peut se produire

  • Le pattern matching de Java 21 introduit aussi le cas de java.lang.MatchException
  • Si un record accessor lève une exception, le switch pattern échoue et une MatchException peut se produire
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);
    }
}
  • Dans cet exemple, comme l’accessor i() lance une ArithmeticException, le bloc switch lance une MatchException
  • Selon JEP 441, un record accessor qui lance systématiquement une exception est un cas très anormal, et il est aussi très rare qu’un pattern switch exhaustif lance une MatchException
  • Même dans un switch exhaustif, une exception peut se produire si aucune variante définie ne correspond au sélecteur
    • JEP 441 décrit le cas d’un switch exhaustif sur un enum qui ne matche plus parce que la classe enum a changé après la compilation du switch
  • Une exception pendant l’évaluation d’une clause de garde peut aussi provoquer une MatchException
static void example(Object obj) {
    switch (obj) {
        case R r when (r.i / 0 == 1): System.out.println("It's an R!");
        default: break;
    }
}

Ce qu’il reste à couvrir

  • En combinant records, sealed types, switch pattern matching et clauses de garde de Java 21, on peut appliquer dans du code Java des briques de base de la programmation fonctionnelle
  • Certains sujets, comme la manière dont les generics interagissent avec les switch patterns, ne sont pas traités ici
  • Le prochain article abordera des particularités et des exemples pratiques pour améliorer la façon d’écrire du code Java

1 commentaires

 
GN⁺ 2023-09-18
Avis de Hacker News
  • La plus grande nouveauté de Java 21, c’est la sortie des threads virtuels : https://openjdk.org/jeps/444
    Pour une raison ou une autre, l’article n’en parle pas. S’il y a une fonctionnalité capable d’attirer des développeurs Go existants vers Java, ce pourrait être celle-ci, et elle pourrait aussi convaincre ceux qui n’aimaient pas les modèles de concurrence de style réactif.

    • Je ne pense pas que les développeurs Go existants reviendront à Java. J’ai travaillé 10 ans en Java avant de passer à Go, et je n’ai aucune intention de revenir en arrière.
      Les applications et bibliothèques Java sont bien trop difficiles à raisonner et à comprendre par rapport à Go, à cause de l’héritage, du packaging, de l’orienté objet, des outils de build, etc.
      Go est simple, facile à comprendre, à lire et à maintenir. Le packaging ressemble à la façon dont on organise des fichiers dans un dossier unique sur un ordinateur, et les outils sont intégrés au langage. On n’a pas non plus l’impression qu’il faut un IDE comme IntelliJ pour que ce soit à peu près utilisable.
      Les choses ont peut-être changé, mais la plupart des bibliothèques Java que je vois aujourd’hui ont encore cet aspect.
    • Avec Java 21, j’attends avec impatience la prochaine version de JRuby intégrant les threads virtuels. Charles Nutter a montré en août, lors d’une présentation JRuby, leur impact sur les fibers Ruby, et c’est assez important.
      J’aime beaucoup de choses dans la JVM et son outillage, mais écrire du code Java ne me plaît plus vraiment. JRuby apporte en quelque sorte le meilleur des deux mondes.
      La présentation est ici, et la démo des threads virtuels commence vers 45 minutes :
      https://youtu.be/pzm6I4liJlg?si=GtxQ4MThEaNDfC67
    • Je ne suis pas sûr que des développeurs Golang existants migrent vers Java à cause de cette fonctionnalité. En revanche, sur l’orientation générale, je me demande pourquoi il y a si peu de conteneurs concurrents dans la communauté Go, et autant dans la communauté Java.
      Même sync.Map n’est pas quelque chose de générique comme le ConcurrentMap de Java : il est spécialisé pour deux cas d’usage précis. Java dispose d’ensembles concurrents, de files, de barrières, de phasers, de pools fork-join, etc. Même avec les goroutines, ce genre de conteneurs reste tout à fait utile ; au moins pour le fork-join, l’implémentation n’est pas si triviale. Utiliser des mutex partout me paraît beaucoup trop bas niveau.
      Je sais qu’il existe des implémentations tierces, mais la concurrence est tellement délicate à bien concevoir que j’hésite à adopter un package tiers s’il n’a pas le niveau de maturité, le grand nombre d’utilisateurs et de développeurs qui soutiennent des projets comme JCTools en Java ou Google Guava.
    • Le contraste entre Executor.newVirtualThreadPerTaskExecutor et go illustre bien pourquoi, selon moi, les développeurs Go ne passeront pas à Java.
      Pour être exact, c’est plutôt try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(...) } à la place de go.
    • Java 21 propose aussi la concurrence structurée en preview (https://openjdk.org/jeps/453). Elle utilise l’implémentation des threads virtuels, et rien qu’à voir les exemples, cela a l’air assez bien ; cela réduit plusieurs douleurs liées à la gestion de la concurrence basée sur les threads.
  • Je pense que le titre du billet de blog était mal choisi. Le sous-titre caché est « Algebraic data types in Java », et il décrit beaucoup mieux le contenu. Un meilleur titre aurait été Algebraic data types in Java 21.
    Probablement à cause du titre, une bonne partie des commentaires ici partent hors sujet. J’aurais aimé voir davantage de discussions sur les types de données algébriques, les avantages et inconvénients de l’implémentation en Java, et des comparaisons techniques avec d’autres langages.

    • J’aimerais qu’il y ait un langage plus populaire où les types de données algébriques sont plus courants, mais je ne suis pas certain de vraiment vouloir voir les types de données algébriques arriver en Java.
      Le code Java existant ne va pas disparaître ; je me demande donc si un mélange aléatoire de ce type de code rendrait vraiment les choses meilleures.
    • Au départ, j’avais choisi ce titre, mais je l’ai changé au dernier moment, et au final cela semble avoir fait dévier l’orientation de l’article.
  • La fonctionnalité sealed classes expliquée ici me semble complètement à côté de la plaque
    Le raisonnement est le suivant : avec une interface ordinaire, n’importe qui peut créer une nouvelle classe qui l’implémente, et du code comme if (x instanceof Foo) { ... } else if (x instanceof Bar) { ... } peut casser à l’exécution si quelqu’un ajoute une nouvelle classe. Donc, avec la nouvelle fonctionnalité d’interface sealed, si l’on empêche quiconque de créer de nouvelles classes implémentant cette interface, l’instruction if ne cassera pas
    Mais la programmation orientée objet n’a-t-elle pas déjà pensé à ce problème et ne l’a-t-elle pas déjà résolu ? Je sais bien que l’orienté objet n’est plus à la mode, mais Java est un langage orienté objet
    La solution consiste à ajouter une méthode à l’interface et à faire en sorte que toutes les classes l’implémentent. Ainsi, au lieu d’énumérer toutes les options dans un énorme if/switch, on appelle la méthode
    Cette approche est meilleure que d’empêcher l’extension du code ; au contraire, elle permet de l’étendre. Le nouvel implémenteur n’a qu’à implémenter cette méthode, et comme le compilateur l’impose, il ne peut pas l’oublier par erreur
    L’exemple des espaces colorimétriques dans l’article (RGB, CMYK, etc.) est un très bon contre-exemple. Si l’on écrit du code qui utilise des espaces colorimétriques, un utilisateur ou un client peut avoir besoin d’un espace colorimétrique étrange et rare auquel je n’avais pas pensé. Je ne veux pas créer du code impossible à étendre à cause d’une structure qui ne prend en charge que les espaces colorimétriques énumérés dans un énorme if/switch

    • La solution consistant à ajouter une méthode à l’interface devient problématique si l’on ne peut pas connaître à l’avance toutes les opérations dont on aura besoin
      Les sealed classes résolvent ce problème. Mais elles introduisent à la place un nouveau problème : « et si l’on a besoin de davantage de classes dérivées et qu’on ne peut pas toutes les connaître à l’avance ? ». Au final, la question est de savoir s’il existe un moyen d’obtenir les deux
      Ce problème s’appelle le problème de l’expression (expression problem) [1]
      Il existe des langages à typage statique capables de résoudre le problème de l’expression, et Java en fait partie [2]. Cela dit, la manière de le faire en Java reste très complexe et pénible, si bien qu’elle est rarement utilisée. Haskell, ou Scala si l’on veut rester dans l’écosystème JVM, font bien mieux sur ce point
      [1] https://en.wikipedia.org/wiki/Expression_problem
      [2] https://koerbitz.me/posts/Solving-the-Expression-Problem-in-...
    • L’approche par sealed classes permet elle aussi à tout le monde d’étendre le code, mais sur une dimension différente de l’approche par méthodes d’interface
      Les méthodes d’interface et les appels virtuels sont très peu flexibles quand on veut ajouter de nouvelles opérations plutôt que de nouvelles classes. Même pour ajouter une seule nouvelle opération, il faut aller ajouter une nouvelle méthode dans toutes les implémentations, ce qui peut casser des implémentations auxquelles on n’a pas accès. Des méthodes sans rapport entre elles doivent aussi être définies dans une même classe, ce qui nuit fortement à la lisibilité du code, et les appels virtuels ne sont pas gratuits : ils ont aussi un impact sur les performances
      Dans ce cas, une sealed class s’étend beaucoup mieux. Il suffit d’ajouter un nouveau switch à un seul endroit, sans casser la compatibilité ascendante
      C’est le fameux problème de l’expression
      https://pkolaczk.github.io/in-defense-of-switch/
    • Je comprends la recommandation d’utiliser la répartition polymorphe plutôt que instanceof, et j’ai vu Bob Martin en parler longuement, mais je ne suis pas d’accord
      Pour faire ce genre de répartition polymorphe, les objets doivent gérer eux-mêmes plusieurs préoccupations
      Dans un jeu vidéo, Car peut avoir .render(), .collide(), .playSound(). Si l’on ajoute ensuite Dog, il suffit d’implémenter ces trois méthodes, sans modifier ni recompiler Renderer, PhysicsEngine ou SoundEngine. Un autre programmeur peut ajouter une entité sans introduire de bug dans mon précieux code. Cela paraît bien
      Mais désormais, Car et Dog doivent tous deux connaître le rendu graphique, la physique et le son. Et les entités n’existent pas isolément. Les voitures et les chiens doivent être rendus dans le bon ordre et peuvent se masquer les uns les autres. Les collisions doivent aussi être vérifiées entre eux. Comme je l’ai vécu dans un vrai game jam, on peut même se retrouver dans une situation où la personne chargée du son doit entrer dans tous les objets pour ajouter le comportement sonore
      Il vaut largement mieux, quand on réfléchit à la physique, travailler dans Physics.collideAll() et faire des traitements spéciaux avec instanceof si nécessaire ; et quand on réfléchit aux graphismes, travailler dans Graphics.renderAll()
      C’est similaire dans le développement web backend Java au quotidien. Quand il faut décider, dans un contrôleur REST, comment transformer un objet Java en réponse HTTP, il est préférable de tout voir dans une seule méthode et de mapper {instanceof Forbidden} vers 403 et {instanceof NotFound} vers 404. Je n’ai pas envie de mettre getCode() ou du contenu spécifique à REST dans la classe Java elle-même
    • Autoriser l’extension n’a pas toujours de sens. Il y a une raison pour laquelle String est final, et on pourrait même considérer que final devrait être la valeur par défaut, avec un mot-clé open explicite uniquement pour les classes dont on autorise la sous-classification
      En programmation fonctionnelle, l’exemple typique d’un type somme est la liste. Elle ne contient que Element(T head, List tail) et Nil(). Il n’y a aucune raison de l’étendre, et l’étendre peut en fait rendre incorrect le code lorsqu’il est combiné avec toutes les fonctions qui manipulent des listes
      Par ailleurs, le patron visiteur, analogue au pattern matching, est très verbeux et repose sur un hack exploitant la sémantique ordinaire de dispatch des méthodes en Java. Ici, je trouve que le pattern matching est plusieurs fois plus lisible
    • Cette fonctionnalité a des cas d’usage valables
      On peut par exemple imaginer une interface de sécurité qui vérifie des jetons de sécurité
      Avec une interface ordinaire, il est facile de l’implémenter pour ignorer les jetons (tout autoriser), les exfiltrer ou ajouter une porte dérobée. Si une telle classe est injectée là où les contrôles de sécurité sont effectués, la sécurité peut être compromise
      Avec une interface sealed, aucune nouvelle implémentation non approuvée ne peut exister. Si l’on reçoit un objet qui prétend implémenter cette interface, on a la garantie qu’il s’agit de l’une des implémentations vérifiées qui effectuent réellement les contrôles de sécurité. Cela élimine d’un coup toute une catégorie de bugs de sécurité et d’exploits
  • C’est un bon article du point de vue de quelqu’un qui connaît les types somme, mais pas très bien les types somme en Java.
    Cela dit, je ne sais pas si les types somme suffiront à me faire aimer Java. La possibilité très large de null demeure, et elle refait surface plusieurs fois dans cet article.

    • En Java, la nullabilité est un gros problème, mais les frameworks de nullabilité fondés sur des annotations sont efficaces et largement répandus dans l’écosystème. Personnellement, je les considère presque indispensables.
      J’attends beaucoup de https://jspecify.dev/, qui vise à standardiser les annotations en partant de @Nullable, avec Google, Meta, Microsoft, etc.
    • Quand Valhalla arrivera, il y aura aussi une nullabilité explicite, donc ce problème devrait être traité.
    • Ce n’est pas encore non plus un langage orienté expression.
  • La réponse de l’article à « pourquoi appelle-t-on ça un product type ? » n’est pas fausse, mais, dit de manière plus intuitive et concise, le nombre total de valeurs possibles d’un product type est le produit du nombre de valeurs possibles des types qui le composent.
    En remplaçant product par sum, cela reste vrai de la même façon.
    Fait intéressant, le nombre total de fonctions distinctes de la forme a -> b, si l’on ne considère que l’entrée et la sortie, se calcule comme une exponentielle. Autrement dit : (nombre de valeurs possibles de b) ^ (nombre de valeurs possibles de a).

    • Pour le dire de manière plus intuitive et concise, un product type équivaut au produit cartésien d’ensembles.
    • Les maps et les listes sont aussi d’autres exemples de types exponentiels. Une fonction pure pouvant, en théorie, être remplacée par une recherche dans une map de valeurs précalculées, l’intuition selon laquelle elles sont équivalentes à des fonctions est naturelle. Dans ce contexte, une liste est une map particulière dont les clés sont des entiers.
      En notation mathématique, pour une liste de Bool, la colonne de gauche correspond au nombre d’éléments et celle de droite au nombre total de possibilités.
      0 : 1
      1 : 2
      2 : 4
      3 : 8
      4 : 16
      5 : 32
      Et ainsi de suite.
  • J’attends que le Project Valhalla soit terminé et que Java reçoive enfin des types valeur. Avec les types somme, les types valeur et les coroutines, cela pourrait en faire un langage tout à fait correct.

  • Java n’a jamais été, à l’origine, un si mauvais langage.
    Le problème, c’étaient les gens. L’énorme suringénierie, les trop nombreuses abstractions qui rendent les bases de code difficiles à comprendre, la magie de code sous forme d’annotations façon instructions GOTO inversées, et les frameworks de DI posaient problème.
    Ce qu’il faut corriger, ce n’est pas le langage, mais l’écosystème. Il faudrait une sorte de mouvement de « Réforme » dans l’écosystème Java.
    Passer à Kotlin, Clojure ou Scala ne suffit pas.

    • Dans n’importe quel langage, on peut créer une HammerFactoryFactory qui produit des HammerFactory. Mais l’écosystème Java encourage et valorise ce genre de manière de résoudre les problèmes. Je pense que C# est assez similaire.
      Ce dont Java a vraiment besoin, c’est de fonctions autonomes, ou de fonctions qualifiées par un namespace. Parfois, on n’a pas besoin d’une classe : une fonction dans un module ou un namespace suffit. Je ne comprends pas pourquoi ce n’est pas possible.
  • L’auteur explique pourquoi les Records sont nécessaires en soulignant que la plupart des objets Java gardent tous leurs champs privés et n’y accèdent que via des méthodes d’accès en lecture et en écriture.
    Mais il n’existe pas de convention imposée au niveau du langage pour définir les accesseurs : on peut appeler le getter de foo getBar, et cela fonctionnera, mais cela risque de dérouter quelqu’un qui veut accéder à bar.
    Scala prend en charge le pattern matching sur les objets qui implémentent une méthode unapply. Cette approche est-elle considérée comme nuisible ? Pourquoi Java n’a-t-il pas suivi cette voie ?

    • C’est encore une question de standardisation. La standardisation de Java est aussi lente que celle de C++. La dernière note de bas de page du JEP sur les record patterns laisse entendre que quelque chose comme unapply pourrait être en préparation, donc tout espoir n’est pas encore perdu.
  • Java a toujours été un excellent langage. Ce qui donne envie de vomir, c’est l’écosystème enterprise. J’ai déjà vu des dizaines de classes et d’interfaces utilisées pour implémenter une seule ligne de logique.