Once Again About ImmutableList in Java

In my previous article, “ Cloaking around the ImmutableList in Java, ” I proposed a solution to the problem of the absence of immutable lists in Java, which is not fixed , neither now nor ever, in the article.


The solution then was worked out only at the level of “there is such an idea”, and the implementation in the code was crooked, therefore everything was perceived somewhat skeptically. In this article, I propose a modified solution. The logic of use and API are brought to an acceptable level. Implementation in code is up to beta level.


Formulation of the problem


We will use the definitions from the original article. In particular, this means that ImmutableList is an immutable list of references to some objects. If these objects are not immutable, then the list will not be an immutable object either, despite the name. In practice, this is unlikely to hurt anyone, but in order to avoid unjustified expectations it is necessary to mention.


It is also clear that the immutability of the list can be "hacked" by means of reflections, or by creating your own classes in the same package, followed by climbing into the protected fields of the list, or something similar.


Unlike the original article, we will not adhere to the principle of “all or nothing”: the author there seems to believe that if the problem cannot be solved at the JDK level, then nothing should be done. (Actually, there’s another question, “cannot be solved” or “the Java authors didn’t have a desire to solve it.” It seems to me that it would still be possible by adding additional interfaces, classes and methods to bring existing collections closer to desired appearance, although less beautiful than if you had thought about it right away, but now it's not about that.)


We will create a library that can successfully coexist with existing collections in Java.


The main ideas of the library:



It should be noted that only lists are considered, since at the moment only they are implemented in the library. But nothing prevents the library from complementing with Set and Map s.


API


ImmutableList


ImmutableList is the successor of ReadOnlyList (which, as in the previous article, is a copied List interface from which all mutating methods are thrown). Methods added:


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

The toList method provides the ability to pass an ImmutableList to pieces of code waiting for a List . A wrapper is returned in which all modifying methods return an UnsupportedOperationException , and the remaining methods are redirected to the original ImmutableList .


The mutable method converts an ImmutableList to a MutableList . A wrapper is returned in which all methods are redirected to the original ImmutableList until the first change. Before the change, the wrapper is untied from the original ImmutableList , copying its contents to the internal ArrayList , to which all operations are then redirected.


The contentEquals method contentEquals intended to compare the contents of a list with the contents of an arbitrary Iterable passed (of course, this operation is meaningful only for those Iterable implementations that have some distinct order of elements).


Note that in our implementation of ReadOnlyList , the iterator and listIterator return standard java.util.Iterator / java.util.ListIterator . These iterators contain modifying methods that will have to be suppressed by throwing an UnsupportedOperationException . It would be preferable to make our ReadOnlyIterator , but in this case we could not write for (Object item : immutableList) , which would immediately spoil all the pleasure of using the library.


MutableList


MutableList is the descendant of the regular List . Methods added:


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

The snapshot method is intended to get a “snapshot” of the current state of MutableList in the form of ImmutableList . The “snapshot” is saved inside the MutableList , and if the state has not changed at the time of the next method call, the same instance of ImmutableList . The “snapshot” stored inside is discarded the first time any modifying method is called, or when releaseSnapshot called. The releaseSnapshot method can be used to save memory if you are sure that no one will need a “snapshot” anymore, but modifying methods will not be called soon.


Mutabor


The Mutabor class provides a set of static methods that are the “entry points” to the library.


Yes, the project is now called “mutabor” (it is consonant with “mutable”, and in translation it means “I will transform”, which is in good agreement with the idea of ​​quickly “transforming” some types of collections into others).


 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* methods copyTo* designed to create appropriate collections by copying the provided data. The convertTo* methods convertTo* for quick conversion of the transferred collection to the desired type, and if it was not possible to quickly convert, they perform slow copying. If the quick conversion was successful, then the original collection is cleared, and it is assumed that it will not be used in the future (although it can, but this hardly makes sense).


The calls to the constructors of the ImmutableList / MutableList implementation ImmutableList MutableList hidden. It is assumed that the user only deals with interfaces, he does not create such objects, and uses the methods described above to transform collections.


Implementation details


ImmutableListImpl


Encapsulates an array of objects. The implementation roughly corresponds to the ArrayList implementation, from which all modifying methods and checks for concurrent modification are thrown.


The implementation of the toList and contentEquals also quite trivial. The toList method returns a wrapper that redirects calls to a given ImmutableList ; slow copying of data does not occur.


The mutable method returns a MutableListImpl created based on this ImmutableList . Data copying does not occur until any modifying method is called on the received MutableList .


MutableListImpl


Encapsulates links to ImmutableList and List . When creating an object, only one of these two links is always filled, the other remains null .


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

Immutable methods redirect calls to ImmutableList if it is not null , and to List otherwise.


Modifying methods redirect calls to List , after initializing:


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

The snapshot method looks like this:


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

The implementation of the releaseSnapshot and contentEquals trivial.


This approach allows you to minimize the number of copies of data during "ordinary" use, replacing copies with fast conversions.


Fast list conversion


Fast conversions are possible for the ArrayList or Arrays$ArrayList classes (the result of the Arrays.asList() method). In practice, in the vast majority of cases, it is precisely these classes that come across.


Inside these classes contain an array of elements. The essence of a quick conversion is to get a reference to this array through reflections (this is a private field) and replace it with a reference to an empty array. This ensures that the only reference to the array remains with our object, and the array remains unchanged.


In the previous version of the library, fast conversions of collection types were performed by calling the constructor. At the same time, the original collection object deteriorated (it became unsuitable for further use), which you do not unconsciously expect from the designer. Now a special static method is used for conversion, and the original collection does not spoil, but is simply cleared. Thus, frightening unusual behavior was eliminated.


Problems with equals / hashCode


Java collections use a very strange approach to implement equals and hashCode methods.


The comparison is carried out according to the content, which seems to be logical, but the class of the list itself is not taken into account. Therefore, for example, ArrayList and LinkedList with the same content will be equals .


Here is the equals / hashCode implementation of AbstractList (from which ArrayList is inherited)
 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; } 

Thus, now absolutely all implementations of List must have a similar implementation of equals (and, as a result, hashCode ). Otherwise, you can get situations when a.equals(b) && !b.equals(a) , which is not good. A similar situation is with Set and Map .


When applied to the library, this means that the implementation of equals and hashCode for MutableList predefined, and in such an implementation, ImmutableList and MutableList with the same contents cannot be equals (since ImmutableList not a List ). Therefore, contentEquals methods have been added to compare content.


The implementation of the equals and hashCode methods for ImmutableList made completely similar to the version from AbstractList , but with the replacement of List by ReadOnlyList .


Total


The library sources and tests are posted by reference in the form of a maven project.


In case someone wants to use the library, he started a group in contact for "feedback".


Using the library is pretty obvious, here is a short example:


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

Good luck to all! Send bug reports!



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


All Articles