In meinem vorherigen Artikel „ Umhüllen von ImmutableList in Java “ habe ich eine Lösung für das Problem des Fehlens unveränderlicher Listen in Java vorgeschlagen, die weder jetzt noch jemals in Java behoben ist .
Die Lösung wurde dann nur auf der Ebene „Es gibt eine solche Idee“ ausgearbeitet, und die Implementierung im Code war schief, daher wurde alles etwas skeptisch wahrgenommen. In diesem Artikel schlage ich eine modifizierte Lösung vor. Die Verwendungslogik und die API werden auf ein akzeptables Niveau gebracht. Die Implementierung in Code erfolgt bis zur Beta-Stufe.
Erklärung des Problems
Wir werden die Definitionen aus dem Originalartikel verwenden. Dies bedeutet insbesondere, dass ImmutableList
eine unveränderliche Liste von Verweisen auf einige Objekte ist. Wenn sich herausstellt, dass diese Objekte nicht unveränderlich sind, ist die Liste trotz des Namens auch kein unveränderliches Objekt. In der Praxis ist es unwahrscheinlich, dass dies jemanden verletzt, aber um ungerechtfertigte Erwartungen zu vermeiden, muss dies erwähnt werden.
Es ist auch klar, dass die Unveränderlichkeit der Liste durch Reflexionen oder durch Erstellen eigener Klassen im selben Paket "gehackt" werden kann, gefolgt vom Klettern in die geschützten Felder der Liste oder ähnliches.
Im Gegensatz zum Originalartikel werden wir uns nicht an das Prinzip „alles oder nichts“ halten: Der Autor dort scheint zu glauben, dass nichts getan werden sollte, wenn das Problem auf JDK-Ebene nicht gelöst werden kann. (Eigentlich eine andere Frage: "Kann nicht gelöst werden" oder "Die Java-Autoren hatten keine Lust, sie zu lösen." Es scheint mir, dass es immer noch möglich wäre, zusätzliche Schnittstellen, Klassen und Methoden hinzuzufügen, um vorhandene Sammlungen näher zu bringen gewünschtes Aussehen, obwohl weniger schön, als wenn Sie sofort darüber nachgedacht hätten, aber jetzt geht es nicht darum.)
Wir werden eine Bibliothek erstellen, die erfolgreich mit vorhandenen Sammlungen in Java koexistieren kann.
Die Hauptideen der Bibliothek:
- Es gibt
ImmutableList
und MutableList
. Durch das Gießen von Typen ist es unmöglich, einen vom anderen zu bekommen. - In unserem Projekt, das wir mithilfe der Bibliothek verbessern möchten, ersetzen wir alle
List
durch eine dieser beiden Schnittstellen. Wenn Sie irgendwann nicht mehr auf die List
, konvertieren wir die List
bei der ersten Gelegenheit von / in eine der beiden Schnittstellen. Gleiches gilt für die Momente des Empfangens / Sendens von Daten an Bibliotheken von Drittanbietern mithilfe von List
. - Gegenseitige Konvertierungen zwischen
ImmutableList
, MutableList
, List
sollten so schnell wie möglich durchgeführt werden ( MutableList
wenn möglich ohne Kopieren von Listen). Ohne „billige“ Roundtrip-Konvertierungen sieht die ganze Idee zweifelhaft aus.
Es ist zu beachten, dass nur Listen berücksichtigt werden, da derzeit nur diese in der Bibliothek implementiert sind. Nichts hindert die Bibliothek jedoch daran, mit Set
und Map
s zu ergänzen.
API
Unveränderliche Liste
ImmutableList
ist der Nachfolger von ReadOnlyList
(das wie im vorherigen Artikel eine kopierte List
, von der alle Mutationsmethoden ausgelöst werden). Methoden hinzugefügt:
List<E> toList(); MutableList<E> mutable(); boolean contentEquals(Iterable<? extends E> iterable);
Die toList
Methode bietet die Möglichkeit, eine ImmutableList
an Codeteile zu übergeben, die auf eine List
warten. Es wird ein Wrapper zurückgegeben, in dem alle Änderungsmethoden eine UnsupportedOperationException
und die verbleibenden Methoden in die ursprüngliche ImmutableList
umgeleitet werden.
Die mutable
Methode konvertiert eine ImmutableList
in eine MutableList
. Es wird ein Wrapper zurückgegeben, in dem alle Methoden bis zur ersten Änderung zur ursprünglichen ImmutableList
umgeleitet werden. Vor der Änderung wird der Wrapper von der ursprünglichen ImmutableList
und sein Inhalt in die interne ArrayList
kopiert, in die dann alle Vorgänge umgeleitet werden.
Die contentEquals
Methode soll den Inhalt der Liste mit dem Inhalt einer beliebigen übergebenen Iterable
(diese Operation ist natürlich nur für Iterable
Implementierungen von Bedeutung, die eine bestimmte Reihenfolge von Elementen aufweisen).
Beachten Sie, dass in unserer Implementierung von listIterator
die iterator
und listIterator
Standard java.util.Iterator
/ java.util.ListIterator
. Diese Iteratoren enthalten Änderungsmethoden, die durch Auslösen einer UnsupportedOperationException
unterdrückt werden müssen. Es wäre vorzuziehen, unseren ReadOnlyIterator
erstellen, aber in diesem Fall konnten wir nicht for (Object item : immutableList)
schreiben, was sofort die Freude an der Nutzung der Bibliothek for (Object item : immutableList)
würde.
MutableList
MutableList
ist der Nachkomme der regulären List
. Methoden hinzugefügt:
ImmutableList<E> snapshot(); void releaseSnapshot(); boolean contentEquals(Iterable<? extends E> iterable);
Die snapshot
Methode wurde entwickelt, um einen "Snapshot" des aktuellen Status von MutableList
als ImmutableList
MutableList
. Der „Snapshot“ wird in der MutableList
. Wenn sich der Status zum Zeitpunkt des nächsten Methodenaufrufs nicht geändert hat, wird dieselbe Instanz von ImmutableList
. Der darin gespeicherte „Snapshot“ wird beim ersten releaseSnapshot
einer Änderungsmethode oder beim releaseSnapshot
. Die releaseSnapshot
Methode kann verwendet werden, um Speicherplatz zu sparen, wenn Sie sicher sind, dass niemand einen „Snapshot“ benötigt, aber Änderungsmethoden nicht bald aufgerufen werden.
Mutabor
Die Mutabor
Klasse bietet eine Reihe statischer Methoden, die die „Einstiegspunkte“ für die Bibliothek darstellen.
Ja, das Projekt heißt jetzt "mutabor" (es steht im Einklang mit "veränderlich" und bedeutet in der Übersetzung "Ich werde transformieren", was gut mit der Idee übereinstimmt, einige Arten von Sammlungen schnell in andere zu "transformieren").
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*
-Methoden copyTo*
zum Erstellen geeigneter Sammlungen durch Kopieren der bereitgestellten Daten. Die convertTo*
-Methoden convertTo*
eine schnelle Konvertierung der übertragenen Sammlung in den gewünschten Typ. Wenn eine schnelle Konvertierung nicht möglich war, führen sie ein langsames Kopieren durch. Wenn die schnelle Konvertierung erfolgreich war, wird die ursprüngliche Sammlung gelöscht, und es wird davon ausgegangen, dass sie in Zukunft nicht mehr verwendet wird (obwohl dies möglich ist, dies jedoch kaum Sinn macht).
Die Aufrufe der Konstruktoren der Implementierungsobjekte ImmutableList
/ MutableList
ausgeblendet. Es wird angenommen, dass der Benutzer sich nur mit Schnittstellen befasst, solche Objekte nicht erstellt und die oben beschriebenen Methoden verwendet, um Sammlungen zu transformieren.
Implementierungsdetails
ImmutableListImpl
Verkapselt ein Array von Objekten. Die Implementierung entspricht in etwa der ArrayList
Implementierung, aus der alle Änderungsmethoden und Überprüfungen auf gleichzeitige Änderung abgeleitet werden.
Die Implementierung der contentEquals
toList
und contentEquals
ebenfalls recht trivial. Die toList
Methode gibt einen Wrapper zurück, der Aufrufe an eine bestimmte ImmutableList
umleitet. Ein langsames Kopieren von Daten findet nicht statt.
Die MutableListImpl
Methode gibt eine MutableListImpl
die basierend auf dieser ImmutableList
. Das Kopieren von Daten erfolgt erst, wenn eine Änderungsmethode für die empfangene MutableList
.
MutableListImpl
Verkapselt Links zu ImmutableList
und List
. Beim Erstellen eines Objekts wird immer nur einer dieser beiden Links gefüllt, der andere bleibt null
.
protected ImmutableList<E> immutable; protected List<E> list;
Unveränderliche Methoden leiten Aufrufe an ImmutableList
wenn sie nicht null
, und andernfalls an List
.
Durch Ändern von Methoden werden Aufrufe nach der Initialisierung an List
umgeleitet:
protected void beforeChange() { if (list == null) { list = new ArrayList<>(immutable.toList()); } immutable = null; }
Die snapshot
Methode sieht folgendermaßen aus:
public ImmutableList<E> snapshot() { if (immutable != null) { return immutable; } immutable = InternalUtils.convertToImmutableList(list); if (immutable != null) {
Die Implementierung der contentEquals
releaseSnapshot
und contentEquals
trivial.
Mit diesem Ansatz können Sie die Anzahl der Kopien von Daten während der "normalen" Verwendung minimieren und Kopien durch schnelle Konvertierungen ersetzen.
Schnelle Listenkonvertierung
Schnelle Konvertierungen sind für die Klassen ArrayList
oder Arrays$ArrayList
(das Ergebnis der Methode Arrays.asList()
). In der Praxis sind in den allermeisten Fällen genau diese Klassen anzutreffen.
In diesen Klassen befindet sich ein Array von Elementen. Das Wesentliche einer schnellen Konvertierung besteht darin, durch Reflexionen einen Verweis auf dieses Array zu erhalten (dies ist ein privates Feld) und durch einen Verweis auf ein leeres Array zu ersetzen. Dadurch wird sichergestellt, dass der einzige Verweis auf das Array bei unserem Objekt verbleibt und das Array unverändert bleibt.
In der vorherigen Version der Bibliothek wurden schnelle Konvertierungen von Sammlungstypen durch Aufrufen des Konstruktors durchgeführt. Gleichzeitig verschlechterte sich das ursprüngliche Sammlungsobjekt (es wurde für die weitere Verwendung ungeeignet), was Sie vom Designer unbewusst nicht erwarten. Jetzt wird eine spezielle statische Methode für die Konvertierung verwendet, und die ursprüngliche Sammlung wird nicht beschädigt, sondern einfach gelöscht. So wurde erschreckendes ungewöhnliches Verhalten beseitigt.
Probleme mit equals / hashCode
Java-Sammlungen verwenden einen sehr seltsamen Ansatz, um equals
und hashCode
Methoden zu implementieren.
Der Vergleich erfolgt nach dem Inhalt, was logisch erscheint, aber die Klasse der Liste selbst wird nicht berücksichtigt. Daher sind beispielsweise ArrayList
und LinkedList
mit demselben Inhalt equals
.
Hier ist die equals / hashCode-Implementierung von AbstractList (von der ArrayList geerbt wird). 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; }
Daher müssen jetzt absolut alle List
Implementierungen eine ähnliche equals
Implementierung haben (und als Ergebnis hashCode
). Andernfalls können Situationen auftreten, in denen a.equals(b) && !b.equals(a)
, was nicht gut ist. Eine ähnliche Situation ist bei Set
und Map
.
In der Anwendung auf die Bibliothek bedeutet dies, dass die Implementierung von equals
und hashCode
für MutableList
vordefiniert ist und in einer solchen Implementierung ImmutableList
und MutableList
mit demselben Inhalt nicht equals
(da ImmutableList
keine List
). Daher wurden contentEquals
Methoden hinzugefügt, um Inhalte zu vergleichen.
Die Implementierung der Methoden equals
und hashCode
für ImmutableList
vollständig der Version von AbstractList
, ersetzt jedoch List
durch ReadOnlyList
.
Insgesamt
Die Bibliotheksquellen und -tests werden als Referenz in Form eines Maven-Projekts veröffentlicht.
Für den Fall, dass jemand die Bibliothek nutzen möchte, hat er eine Kontaktgruppe für „Feedback“ erstellt.
Die Verwendung der Bibliothek ist ziemlich offensichtlich. Hier ein kurzes Beispiel:
private boolean myBusinessProcess() { List<Entity> tempFromDb = queryEntitiesFromDatabase("SELECT * FROM my_table"); ImmutableList<Entity> fromDb = Mutabor.convertToImmutableList(tempFromDb); if (fromDb.isEmpty() || !someChecksPassed(fromDb)) { return false; }
Viel Glück an alle! Fehlerberichte senden!