Encore une fois à propos d'ImmutableList en Java

Dans mon article précédent, « Cloaking around ImmutableList in Java », j'ai proposé une solution au problème de l'absence de listes immuables en Java, qui n'est pas corrigé , ni maintenant ni jamais , en Java.


La solution n'a alors été élaborée qu'au niveau «il y a une telle idée», et l'implémentation dans le code était tordue, donc tout était quelque peu sceptique. Dans cet article, je propose une solution modifiée. La logique d'utilisation et l'API sont portées à un niveau acceptable. L'implémentation dans le code est jusqu'au niveau bêta.


Énoncé du problème


Nous utiliserons les définitions de l'article d'origine. En particulier, cela signifie que ImmutableList est une liste immuable de références à certains objets. Si ces objets s'avèrent non immuables, alors la liste ne sera pas non plus un objet immuable, malgré le nom. Dans la pratique, il est peu probable que cela nuise à qui que ce soit, mais pour éviter des attentes injustifiées, il convient de le mentionner.


Il est également clair que l'immuabilité de la liste peut être "piratée" au moyen de réflexions, ou en créant vos propres classes dans le même package, puis en grimpant dans les champs protégés de la liste, ou quelque chose de similaire.


Contrairement à l'article d'origine, nous n'adhérerons pas au principe du «tout ou rien»: l'auteur semble croire que si le problème ne peut être résolu au niveau du JDK, alors rien ne devrait être fait. (En fait, il y a une autre question, "ne peut pas être résolue" ou "les auteurs Java n'avaient pas le désir de le résoudre". Il me semble qu'il serait toujours possible en ajoutant des interfaces, des classes et des méthodes supplémentaires pour rapprocher les collections existantes de l'apparence souhaitée, bien que moins belle que si vous y aviez pensé tout de suite, mais maintenant ce n'est pas à ce sujet.)


Nous allons créer une bibliothèque qui peut coexister avec succès avec les collections existantes en Java.


Les principales idées de la bibliothèque:


  • Il existe des MutableList et MutableList . En lançant des types, il est impossible d'obtenir l'un de l'autre.
  • Dans notre projet, que nous souhaitons améliorer à l'aide de la bibliothèque, nous remplaçons tous les List par l'une de ces deux interfaces. Si, à un moment donné, vous ne pouvez pas vous passer de la List , à la première occasion, nous convertirons la List de / vers l'une des deux interfaces. Il en va de même pour les moments de réception / transmission de données à des bibliothèques tierces à l'aide de List .
  • Les conversions mutuelles entre MutableList , MutableList , List doivent être effectuées aussi rapidement que possible (c'est-à-dire sans copier les listes, si possible). Sans conversions aller-retour «bon marché», l'idée commence à paraître douteuse.

Il convient de noter que seules les listes sont prises en compte, car pour l'instant seules elles sont implémentées dans la bibliothèque. Mais rien n'empêche la bibliothèque de compléter avec Set et Map s.


API


ImmutableList


ImmutableList est le successeur de ReadOnlyList (qui, comme dans l'article précédent, est une interface List copiée à partir de laquelle toutes les méthodes de mutation sont supprimées). Méthodes ajoutées:


 List<E> toList(); MutableList<E> mutable(); boolean contentEquals(Iterable<? extends E> iterable); 

La méthode toList offre la possibilité de passer une ImmutableList à des morceaux de code en attente d'une List . Un wrapper est retourné dans lequel toutes les méthodes de modification renvoient une UnsupportedOperationException , et les méthodes restantes sont redirigées vers la liste ImmutableList origine.


La méthode mutable convertit une MutableList une MutableList . Un encapsuleur est renvoyé dans lequel toutes les méthodes sont redirigées vers la liste ImmutableList origine jusqu'à la première modification. Avant la modification, l'encapsuleur est délié de la liste ImmutableList origine, copiant son contenu dans la liste ArrayList interne, vers laquelle toutes les opérations sont ensuite redirigées.


La méthode contentEquals destinée à comparer le contenu de la liste avec le contenu d'un Iterable passé arbitrairement (bien sûr, cette opération n'a de sens que pour les implémentations Iterable qui ont un ordre distinct d'éléments).


Notez que dans notre implémentation de ReadOnlyList , les listIterator iterator et listIterator renvoient la norme java.util.Iterator / java.util.ListIterator . Ces itérateurs contiennent des méthodes de modification qui devront être supprimées en lançant une UnsupportedOperationException . Il serait préférable de faire notre ReadOnlyIterator , mais dans ce cas nous ne pourrions pas écrire for (Object item : immutableList) , ce qui gâterait immédiatement tout le plaisir d'utiliser la bibliothèque.


MutableList


MutableList est le descendant de la List régulière. Méthodes ajoutées:


 ImmutableList<E> snapshot(); void releaseSnapshot(); boolean contentEquals(Iterable<? extends E> iterable); 

La méthode d' snapshot est conçue pour obtenir un «instantané» de l'état actuel de MutableList tant que MutableList . Le «snapshot» est enregistré à l'intérieur de MutableList , et si l'état n'a pas changé au moment du prochain appel de méthode, la même instance de ImmutableList . Le «snapshot» stocké à l'intérieur est ignoré la première fois qu'une méthode de modification est appelée, ou lorsque releaseSnapshot appelé. La méthode releaseSnapshot peut être utilisée pour économiser de la mémoire si vous êtes sûr que personne n'aura besoin d'un «instantané», mais les méthodes de modification ne seront pas appelées bientôt.


Mutabor


La classe Mutabor fournit un ensemble de méthodes statiques qui sont les «points d'entrée» de la bibliothèque.


Oui, le projet s'appelle désormais «mutabor» (il est en accord avec «mutable», et en traduction cela signifie «je vais transformer», ce qui est en bon accord avec l'idée de «transformer» rapidement certains types de collections en d'autres).


 public static <E> ImmutableList<E> copyToImmutableList(E[] original); public static <E> ImmutableList<E> copyToImmutableList(Collection<? extends E> original); public static <E> ImmutableList<E> convertToImmutableList(Collection<? extends E> original); public static <E> MutableList<E> copyToMutableList(Collection<? extends E> original); public static <E> MutableList<E> convertToMutableList(List<E> original); 

copyTo* méthodes copyTo* conçues pour créer des collections appropriées en copiant les données fournies. Les méthodes convertTo* une conversion rapide de la collection transférée vers le type souhaité, et s'il n'était pas possible de convertir rapidement, elles effectuent une copie lente. Si la conversion rapide a réussi, la collection d'origine est supprimée et il est supposé qu'elle ne sera pas utilisée à l'avenir (bien que cela soit possible, mais cela n'a guère de sens).


Les appels aux constructeurs des ImmutableList implémentation MutableList / MutableList masqués. Il est supposé que l'utilisateur ne traite que des interfaces, il ne crée pas de tels objets et utilise les méthodes décrites ci-dessus pour transformer les collections.


Détails d'implémentation


ImmutableListImpl


Encapsule un tableau d'objets. L'implémentation correspond à peu près à l'implémentation ArrayList , à partir de laquelle toutes les méthodes de modification et vérifie les modifications simultanées sont rejetées.


L'implémentation des contentEquals toList et contentEquals également assez triviale. La méthode toList retourne un wrapper qui redirige les appels vers une ImmutableList donnée; la copie lente des données ne se produit pas.


La méthode mutable renvoie un MutableListImpl créé sur la base de cet ImmutableList . La copie des données ne se produit que MutableList méthode de modification est appelée sur la liste MutableList reçue.


MutableListImpl


Encapsule les liens vers ImmutableList et List . Lors de la création d'un objet, un seul de ces deux liens est toujours rempli, l'autre reste null .


 protected ImmutableList<E> immutable; protected List<E> list; 

Les méthodes immuables redirigent les appels vers ImmutableList s'il n'est pas null et vers List cas contraire.


La modification des méthodes redirige les appels vers List , après l'initialisation:


 protected void beforeChange() { if (list == null) { list = new ArrayList<>(immutable.toList()); } immutable = null; } 

La méthode de l' snapshot ressemble à ceci:


 public ImmutableList<E> snapshot() { if (immutable != null) { return immutable; } immutable = InternalUtils.convertToImmutableList(list); if (immutable != null) { //    //   ,  . //     immutable     . list = null; return immutable; } immutable = InternalUtils.copyToImmutableList(list); return immutable; } 

L'implémentation des contentEquals et contentEquals triviale.


Cette approche vous permet de minimiser le nombre de copies de données lors d'une utilisation "ordinaire", en remplaçant les copies par des conversions rapides.


Conversion de liste rapide


Des conversions rapides sont possibles pour les classes ArrayList ou Arrays$ArrayList (résultat de la méthode Arrays.asList() ). En pratique, dans la grande majorité des cas, ce sont précisément ces classes qui se rencontrent.


À l'intérieur de ces classes contiennent un tableau d'éléments. L'essence d'une conversion rapide est d'obtenir une référence à ce tableau par le biais de réflexions (il s'agit d'un champ privé) et de le remplacer par une référence à un tableau vide. Cela garantit que la seule référence au tableau reste avec notre objet et que le tableau reste inchangé.


Dans la version précédente de la bibliothèque, des conversions rapides des types de collection étaient effectuées en appelant le constructeur. Dans le même temps, l'objet de collection d'origine s'est détérioré (il est devenu impropre à une utilisation ultérieure), ce que vous n'attendez pas inconsciemment du designer. Maintenant, une méthode statique spéciale est utilisée pour la conversion, et la collection d'origine ne se gâte pas, mais est simplement effacée. Ainsi, un comportement inhabituel effrayant a été éliminé.


Problèmes avec equals / hashCode


Les collections Java utilisent une approche très étrange pour implémenter les méthodes equals et hashCode .


La comparaison est effectuée en fonction du contenu, ce qui semble logique, mais la classe de la liste elle-même n'est pas prise en compte. Par conséquent, par exemple, ArrayList et LinkedList avec le même contenu seront equals .


Voici l'implémentation equals / hashCode de AbstractList (dont ArrayList est hérité)
 public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof List)) return false; ListIterator<E> e1 = listIterator(); ListIterator e2 = ((List) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { E o1 = e1.next(); Object o2 = e2.next(); if (!(o1==null ? o2==null : o1.equals(o2))) return false; } return !(e1.hasNext() || e2.hasNext()); } public int hashCode() { int hashCode = 1; for (E e : this) hashCode = 31*hashCode + (e==null ? 0 : e.hashCode()); return hashCode; } 

Ainsi, désormais, toutes les implémentations de List doivent avoir une implémentation d' equals similaire (et, par conséquent, hashCode ). Sinon, vous pouvez obtenir des situations où a.equals(b) && !b.equals(a) , ce qui n'est pas bon. Une situation similaire est avec Set et Map .


Lorsqu'il est appliqué à la bibliothèque, cela signifie que l'implémentation de equals et hashCode pour MutableList prédéfinie, et dans une telle implémentation, MutableList et MutableList avec le même contenu ne peuvent pas être equals (puisque ImmutableList pas une List ). Par conséquent, les méthodes contentEquals ont été ajoutées pour comparer le contenu.


L'implémentation des méthodes equals et hashCode pour ImmutableList complètement similaire à la version de AbstractList , mais avec le remplacement de List par ReadOnlyList .


Total


Les sources et tests de la bibliothèque sont affichés par référence sous la forme d'un projet maven.


Dans le cas où quelqu'un souhaite utiliser la bibliothèque, il a créé un groupe en contact pour des «retours».


L'utilisation de la bibliothèque est assez évidente, voici un petit exemple:


 private boolean myBusinessProcess() { List<Entity> tempFromDb = queryEntitiesFromDatabase("SELECT * FROM my_table"); ImmutableList<Entity> fromDb = Mutabor.convertToImmutableList(tempFromDb); if (fromDb.isEmpty() || !someChecksPassed(fromDb)) { return false; } //... MutableList<Entity> list = fromDb.mutable(); //time to change list.remove(1); ImmutableList<Entity> processed = list.snapshot(); //time to change ended //... if (!callSideLibraryExpectsListParameter(processed.toList())) { return false; } for (Entity entity : processed) { outputToUI(entity); } return true; } 

Bonne chance à tous! Envoyez des rapports de bogues!

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


All Articles