Il n'y aura pas de collections immuables en Java - ni maintenant ni jamais

Bonjour à tous!

Aujourd'hui, votre attention est invitée à la traduction d'un article soigneusement écrit sur l'un des problèmes de base de Java - la mutabilité, et comment il affecte la structure des structures de données et comment travailler avec elles. Le matériel est tiré du blog de Nicolai Parlog, dont nous avons vraiment essayé de garder le style littéraire brillant en traduction. Nicolas lui-même est remarquablement caractérisé par un extrait du blog de la société JUG.ru sur Habré; citons ce passage dans son intégralité:


Nikolay Parlog est un tel mec des médias qui fait des critiques sur les fonctionnalités Java. Mais il n'est pas d'Oracle en même temps, donc les critiques sont étonnamment franches et compréhensibles. Parfois après eux, quelqu'un est renvoyé, mais rarement. Nikolay parlera de l'avenir de Java, ce qui sera dans la nouvelle version. Il est bon pour parler des tendances et généralement du grand monde. C'est un compagnon très bien lu et érudit. Même les rapports simples sont agréables à écouter, tout le temps que vous apprenez quelque chose de nouveau. De plus, Nicolas sait au-delà de ce qu'il raconte. Autrement dit, vous pouvez venir à n'importe quel rapport et en profiter, même si ce n'est pas du tout votre sujet. Il enseigne. Il a écrit «The Java Module System» pour Manning Publishing House, tient des blogs sur le développement de logiciels sur codefx.org, et a longtemps été impliqué dans plusieurs projets open source. Il peut être embauché dès la conférence, il est pigiste. Certes, un pigiste très cher. Voici le rapport .

Nous lisons et votons. Qui aime particulièrement le message - nous vous recommandons également de consulter les commentaires du lecteur sur le message d'origine.

La volatilité est mauvaise, non? Par conséquent, l' immuabilité est bonne . Les principales structures de données dans lesquelles l'immuabilité est particulièrement fructueuse sont les collections: en Java c'est une liste ( List ), un ensemble ( Set ) et un dictionnaire ( Map ). Cependant, bien que le JDK soit livré avec des collections immuables (ou non modifiables?), Le système de type n'en sait rien. Il n'y a pas d' ImmutableList dans le JDK, et ce type de Guava me semble complètement inutile. Mais pourquoi? Pourquoi ne pas simplement ajouter Immutable... à ce mélange et ne pas dire qu'il devrait l'être?

Qu'est-ce qu'une collection immuable?


Dans la terminologie JDK, la signification des mots « immuable » et « non modifiable » a changé au cours des dernières années. Initialement, «non modifiable» était appelé une instance qui ne permettait pas la mutabilité (mutabilité): en réponse à l'évolution des méthodes, il a lancé UnsupportedOperationException . Cependant, il pourrait être changé d'une autre manière - peut-être parce qu'il ne s'agissait que d'un wrapper autour d'une collection mutable. Ces vues sont reflétées dans les méthodes Collections::unmodifiableList , unmodifiableSet et unmodifiableMap , ainsi que dans leur JavaDoc .

Initialement, le terme " immuable " fait référence aux collections retournées par les méthodes d'usine des collections Java 9 . Les collections elles-mêmes ne pouvaient en aucun cas être modifiées (oui, il y a de la réflexion , mais cela ne compte pas), il semble donc qu'elles justifient leur nom. Hélas, la confusion survient souvent à cause de cela. Supposons qu'il existe une méthode qui affiche tous les éléments d'une collection immuable à l'écran - donnera-t-elle toujours le même résultat? Hein? Ou pas?

Si vous n’avez pas immédiatement répondu non , cela signifie que vous venez de voir exactement quelle confusion est possible ici. Une « collection immuable d'agents secrets » - cela semblerait sacrément similaire à une « collection immuable d'agents secrets immuables », mais ces deux entités peuvent ne pas être identiques. Une collection immuable ne peut pas être modifiée à l'aide des opérations d'insertion / suppression / nettoyage, etc., mais si les agents secrets sont mutables (bien que les traits de caractère dans les films d'espionnage soient si mauvais qu'ils n'y croient pas vraiment), cela ne signifie pas pour autant que toute la collection d'agents secrets est immuable. Par conséquent, il y a maintenant un changement vers le fait de nommer de telles collections non modifiables , plutôt qu'immuables , qui est inscrit dans la nouvelle édition de JavaDoc .

Les collections immuables discutées dans cet article peuvent contenir des éléments mutables.

Personnellement, je n'aime pas une telle révision de la terminologie. À mon avis, l'expression «collection immuable» ne devrait signifier que la collection elle-même ne peut pas être modifiée, mais ne doit pas caractériser les éléments qu'elle contient. Dans ce cas, il y a un autre point positif: le terme «immuabilité» dans l'écosystème Java ne se transforme pas en un non-sens complet.

D'une manière ou d'une autre, dans cet article, nous parlerons de collections immuables , où ...

  • Les instances contenues dans la collection sont déterminées au stade de la conception
  • Ces copies - un montant égal, ni réduire ni ajouter
  • Aucune déclaration n'est faite concernant la mutabilité de ces éléments.

Insistons sur le fait que maintenant nous allons pratiquer l'ajout de collections immuables. Pour être précis - une liste immuable. Tout ce qui sera dit sur les listes est également applicable aux collections d'autres types.

Commencer à ajouter des collections immuables!

Créons l'interface ImmutableList et faisons-la, par rapport à List , euh ... quoi? Supertype ou sous-type? Arrêtons-nous sur la première option.



Magnifiquement, ImmutableList pas de méthodes ImmutableList , donc son utilisation est toujours sûre, non? Alors?! Non, monsieur.

 List<Agent> agents = new ArrayList<>(); // ,  `List`  `ImmutableList` ImmutableList<Agent> section4 = agents; //    section4.forEach(System.out::println); //    `section4` agents.add(new Agent("Motoko"); //  "Motoko" – ,      ?! section4.forEach(System.out::println); 

Cet exemple montre qu'il est possible de transférer une telle liste non entièrement immuable vers l'API, dont le fonctionnement peut être construit sur l'immuabilité et, par conséquent, annule toutes les garanties qu'un nom de ce type peut faire allusion. Voici une recette pour vous qui pourrait conduire à un désastre.

OK, puis ImmutableList étend la List . Peut-être?



Maintenant, si l'API attend une liste immuable, elle recevra alors une telle liste, mais il y a deux inconvénients:

  • Les listes immuables devraient toujours proposer des méthodes de modification (telles qu'elles sont définies dans le supertype), et la seule implémentation possible lèvera une exception
  • ImmutableList instances ImmutableList également des instances List , et lorsqu'elles sont affectées à une telle variable, passées comme tel argument ou renvoyées de ce type, il est logique de supposer que la mutabilité est autorisée.

Ainsi, il s'avère que vous ne pouvez utiliser ImmutableList localement, car il passe les frontières de l'API sous forme de List , ce qui vous oblige à un niveau de précaution surhumain, ou explose au moment de l'exécution. Ce n'est pas aussi mauvais qu'une List étendant une ImmutableList , mais une telle solution est encore loin d'être idéale.

C'est exactement ce que j'avais en tête quand j'ai dit que le type ImmutableList de Goyave était pratiquement inutile. Il s'agit d'un excellent exemple de code, très fiable lorsque vous travaillez avec des listes locales immuables (c'est pourquoi je l'utilise activement), mais, en y ayant recours, il est très facile d'aller au-delà de la citadelle compilée imprenable et garantie, dont les murs étaient composés de types immuables - et uniquement dans ce les types immuables peuvent révéler pleinement leur potentiel. C'est mieux que rien, mais inefficace en tant que solution JDK.

Si l' ImmutableList ne peut pas étendre la List et que la solution de contournement ne fonctionne toujours pas, alors comment est-il supposé que tout fonctionne?

L'immuabilité est une caractéristique


Le problème que nous avons rencontré lors des deux premières tentatives pour ajouter des types immuables était notre idée fausse que l'immuabilité est simplement l'absence de quelque chose: nous prenons une List , en supprimons le code changeant, nous obtenons une ImmutableList . Mais, en fait, tout cela ne fonctionne pas de cette façon.

Si nous supprimons simplement les méthodes de modification de la List , nous obtenons une liste en lecture seule. Ou, en respectant la terminologie formulée ci-dessus, elle peut être appelée UnmodifiableList - elle peut encore changer, mais vous ne la modifierez pas.

Maintenant, nous pouvons ajouter deux choses à cette image:

  • Nous pouvons le rendre mutable en ajoutant des méthodes appropriées
  • Nous pouvons le rendre immuable en ajoutant des garanties appropriées.

L'immuabilité n'est pas l'absence de mutabilité, mais une garantie qu'il n'y aura pas de changements
Dans ce cas, il est important de comprendre que dans les deux cas, nous parlons de fonctionnalités complètes - l'immuabilité n'est pas l'absence de changements, mais une garantie qu'il n'y aura pas de changements. Une fonctionnalité existante peut ne pas être nécessairement utilisée à bon escient, elle peut également garantir que quelque chose de mauvais ne se produira pas dans le code - dans ce cas, pensez, par exemple, à la sécurité des threads.

De toute évidence, la mutabilité et l'immuabilité entrent en conflit, et nous ne pouvons donc pas utiliser simultanément les deux hiérarchies d'héritage susmentionnées. Les types héritent des capacités des autres types, donc peu importe comment vous les coupez, si l'un des types hérite de l'autre, il contiendra les deux fonctionnalités.

Donc, List et ImmutableList ne peuvent pas s'étendre. Mais nous avons été amenés ici en travaillant avec UnmodifiableList , et il s'avère vraiment que les deux types ont la même API, en lecture seule, ce qui signifie qu'ils doivent l'étendre.



Bien que je n'appellerais pas les choses exactement de ces noms, la hiérarchie de ce type est raisonnable. À Scala, par exemple, cela se fait pratiquement . La différence est que le supertype partagé, que nous avons appelé UnmodifiableList , définit des méthodes mutables qui renvoient une collection modifiée et laissent l'original intact. Ainsi, une liste immuable se révèle être persistante et donne à une variante mutable deux ensembles de méthodes de modification - héritées pour recevoir des copies modifiées et les siennes pour les changements en place.

Et Java? Est-il possible de moderniser une telle hiérarchie en y ajoutant de nouveaux supertypes et frères et sœurs?

Est-il possible d'améliorer les collections non modifiables et immuables?
Bien sûr, il n'y a aucun problème à ajouter les ImmutableList et ImmutableList et à créer la hiérarchie d'héritage décrite ci-dessus. Le problème est qu'à court et moyen terme, il sera pratiquement inutile. Laisse-moi t'expliquer.

Le ImmutableList d'avoir UnmodifiableList , ImmutableList et List comme types - dans ce cas, les API seront en mesure d'exprimer clairement ce dont elles ont besoin et ce qu'elles offrent.

 public void payAgents(UnmodifiableList<Agent> agents) { //      , //         } public void sendOnMission(ImmutableList<Agent> agents) { //   ( ), //  ,     } public void downtime(List<Agent> agents) { //       , //        ,      } public UnmodifiableList<Agent> teamRoster() { //   ,     , //      ,     -  } public ImmutableList<Agent> teamOnMission() { //    ,      } public List<Agent> team() { //    ,    , //        } 

Cependant, à moins que vous ne commenciez le projet à partir de zéro, vous aurez probablement une telle fonctionnalité, et elle ressemblera à ceci:

 //   ,  `Iterable<Agent>` //  ,   ,        public void payAgents(List<Agent> agents) { } public void sendOnMission(List<Agent> agents) { } public void downtime(List<Agent> agents) { } //      , //    ,  `List`     public List<Agent> teamRoster() { } // ,     `Stream<Agent>` public List<Agent> teamOnMission() { } public List<Agent> team() { } 

Ce n'est pas bon, car pour que les nouvelles collections que nous venons de présenter soient utiles, nous devons travailler avec elles! (ouf). Ce qui précède n'est pas sans rappeler le code de l'application, par conséquent, la refactorisation ImmutableList UnmodifiableList et ImmutableList , et vous pouvez l'implémenter, comme indiqué dans la liste ci-dessus. Cela peut être un gros morceau de travail, couplé à de la confusion lorsque vous devez organiser l'interaction du code ancien et mis à jour, mais au moins cela semble faisable.

Qu'en est-il des frameworks, des bibliothèques et du JDK lui-même? Ici, tout semble sombre. Une tentative de modification du paramètre ou du type de retour de List à ImmutableList entraînera une incompatibilité avec le code source , c'est-à-dire le code source existant ne sera pas compilé avec la nouvelle version, car ces types ne sont pas liés les uns aux autres. De même, la modification du type de retour de List en un nouveau supertype UnmodifiableList entraînera des erreurs de compilation.

Avec l'introduction de nouveaux types, il sera nécessaire de faire des changements et de recompiler dans tout l'écosystème.

Cependant, même si nous développons le type de paramètre de List à UnmodifiableList , nous rencontrerons un problème, car une telle modification entraîne une incompatibilité au niveau du bytecode . Lorsque le code source appelle une méthode, le compilateur convertit cet appel en bytecode qui référence la méthode cible en:

  • Le nom de la classe dont l'instance la cible est déclarée
  • Nom de la méthode
  • Types de paramètres de méthode
  • Type de retour de méthode

Toute modification du paramètre ou du type de retour de la méthode entraînera le bytecode à indiquer la mauvaise signature lors de la référence à la méthode; en conséquence, une erreur NoSuchMethodError se produit pendant l'exécution. Si la modification que vous apportez est compatible avec le code source - par exemple, si vous parlez de restreindre le type de retour ou d'étendre le type du paramètre - alors la recompilation devrait être suffisante. Cependant, avec des changements profonds, par exemple, avec l'introduction de nouvelles collections, tout n'est pas si simple: pour consolider ces changements, vous devez recompiler tout l'écosystème Java. C'est une entreprise perdue.

La seule façon envisageable de tirer parti de ces nouvelles collections sans rompre la compatibilité est de dupliquer chaque méthode existante avec un nouveau nom, de modifier l'API, puis de marquer l'ancienne version comme indésirable. Pouvez-vous imaginer à quel point une telle tâche serait monumentale et pratiquement infinie?!

La réflexion


Bien sûr, les types de collection immuables sont une grande chose que j'aimerais avoir, mais nous ne verrons probablement pas quelque chose comme ça dans le JDK. Les implémentations compétentes de List et d' ImmutableList ne ImmutableList jamais se développer (en fait, les deux étendent le même type de liste UnmodifiableList , qui est en lecture seule), ce qui rend difficile l'intégration de ces types dans les API existantes.

En dehors du contexte de toute relation spécifique entre les types, la modification des signatures de méthode existantes est toujours lourde de problèmes, car ces modifications sont incompatibles avec le bytecode. Lorsqu'ils sont introduits, au moins une recompilation est nécessaire, et il s'agit d'un changement dévastateur qui affectera l'ensemble de l'écosystème Java.

Par conséquent, je crois que rien de tel ne se produira - jamais et jamais.

Source: https://habr.com/ru/post/fr470149/


All Articles