Noch einmal über ImmutableList in Java

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) { //    //   ,  . //     immutable     . list = null; return immutable; } immutable = InternalUtils.copyToImmutableList(list); return immutable; } 

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; } //... 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; } 

Viel Glück an alle! Fehlerberichte senden!

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


All Articles