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:
- There are
ImmutableList
andMutableList
. By casting types it is impossible to get one from the other. - In our project, which we want to improve using the library, we replace all the
List
s with one of these two interfaces. If at some point you cannot do without theList
, then at the first opportunity we will convert theList
from / to one of two interfaces. The same applies to the moments of receiving / transmitting data to third-party libraries usingList
. - Mutual conversions between
ImmutableList
,MutableList
,List
should be performed as quickly as possible (that is, without copying lists, if possible). Without "cheap" round-trip conversions, the whole idea begins to look dubious.
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
.
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!