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) {
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; }
Bonne chance à tous! Envoyez des rapports de bogues!