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<>();
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) {
Cependant, à moins que vous ne commenciez le projet à partir de zéro, vous aurez probablement une telle fonctionnalité, et elle ressemblera à ceci:
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.