I read the article “There will be no immutable collections in Java - neither now, nor ever” and thought that the problem of the absence of immutable lists in Java, which makes the author sad, is quite solvable on a limited scale. I offer my thoughts and pieces of code on this subject.
(This is an answer article, read the original article first.)
The first question that arose is: why do I need a UnmodifiableList
, if there is an ImmutableList
? As a result of the discussion, two ideas regarding the meaning of UnmodifiableList
are seen in the comments of the original article:
UnmodifiableList
UnmodifiableList
ImmutableList
UnmodifiableList
The first option seems too rare in practice. Thus, if it is possible to make an “easy” implementation of ImmutableList
, then UnmodifiableList
becomes not very necessary. Therefore, in the future, we will forget about it and will only implement ImmutableList
.
We will implement the ImmutableList
option:
List
ImmutableList
List
ArrayList
First, we deal with the API. We examine the Collection
and List
interfaces and copy the “reading” part from them into our new interfaces.
public interface ReadOnlyCollection<E> extends Iterable<E> { int size(); boolean isEmpty(); boolean contains(Object o); Object[] toArray(); <T> T[] toArray(T[] a); boolean containsAll(Collection<?> c); } public interface ReadOnlyList<E> extends ReadOnlyCollection<E> { E get(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); ReadOnlyList<E> subList(int fromIndex, int toIndex); }
Next, create the ImmutableList
class. The signature is similar to ArrayList
(but implements the ReadOnlyList
interface instead of List
).
public class ImmutableList<E> implements ReadOnlyList<E>, RandomAccess, Cloneable, Serializable
We copy the implementation of the class from ArrayList
and firmly refactor, throwing out everything related to the "writing" part, checking for concurrent modification, etc.
Constructors will be as follows:
public ImmutableList() public ImmutableList(E[] original) public ImmutableList(Collection<? extends E> original)
The first creates an empty list. The second creates a list by copying the array. We can’t do without copying if we want to achieve immutable. The third is more interesting. A similar ArrayList
constructor also copies data from the collection. We will do the same, unless orginal
is an instance of ArrayList
or Arrays$ArrayList
(this is what is returned by the Arrays.asList()
method). We can safely assume that these cases will cover 90% of the constructor calls.
In these cases, we will "steal" the original
array through reflections (there is hope that this is faster than copying gigabyte arrays). The essence of "theft":
original
ArrayList.elementData
protected static final Field data_ArrayList; static { try { data_ArrayList = ArrayList.class.getDeclaredField("elementData"); data_ArrayList.setAccessible(true); } catch (NoSuchFieldException | SecurityException e) { throw new IllegalStateException(e); } } public ImmutableList(Collection<? extends E> original) { Object[] arr = null; if (original instanceof ArrayList) { try { arr = (Object[]) data_ArrayList.get(original); data_ArrayList.set(original, null); } catch (@SuppressWarnings("unused") IllegalArgumentException | IllegalAccessException e) { arr = null; } } if (arr == null) { // ArrayList, - arr = original.toArray(); } this.data = arr; }
As a contract, we assume that when the constructor is called, the mutable list is ImmutableList
to an ImmutableList
. The original list cannot be used after that. When trying to use, a NullPointerException
arrives. This ensures that the "stolen" array will not change and our list will be really immutable (except for the option when someone gets to the array through reflections).
Suppose we decide to use an ImmutableList
in a real project.
The project interacts with libraries: receives from them and sends them various lists. In the vast majority of cases, these lists will be an ArrayList
. The described implementation of ImmutableList
allows ImmutableList
to quickly convert the resulting ArrayList
to an ImmutableList
. It is also required to implement conversion for lists sent to libraries: ImmutableList
to List
. For fast conversion, you need an ImmutableList
wrapper that implements List
, throwing exceptions when trying to write to the list (similar to Collections.unmodifiableList
).
Also, the project itself somehow processes the lists. It makes sense to create a MutableList
class that represents a mutable list, with an implementation based on ArrayList
. In this case, you can refactor the project by substituting instead of all ArrayList
class that explicitly declares the intent: either ImmutableList
or MutableList
.
Need a quick conversion from ImmutableList
to MutableList
and vice versa. At the same time, unlike the conversion of ArrayList
to ImmutableList
, we can no longer spoil the original list.
Converting there will usually be slow, with copying the array. But for cases when the received MutableList
does not always change, you can make a wrapper: MutableList
, which saves a link to the ImmutableList
and uses it for "reading" methods, and if the "writing" method is called, then only forgets about the ImmutableList
, after copying the contents of it array to itself, and then it already works with its array (something remotely similar is in CopyOnWriteArrayList
).
Converting "back" means receiving a snapshot of the contents of the MutableList
at the time the method is called. Again, in most cases you cannot do without copying an array, but you can make a wrapper to optimize cases of several conversions between which the contents of the MutableList
did not change. Another option for converting "back": some data is collected in a MutableList
, and when the data collection is completed, a MutableList
needs to be converted forever to an ImmutableList
. It is also implemented without problems with another wrapper.
The results of the experiment in the form of code are posted here
ImmutableList
itself is ImmutableList
, described in the "Other classes" section (yet?) Is not implemented.
We can assume that the premise of the original article, "immutable collections in Java will not be" is erroneous.
If there is a desire, then it is quite possible to use a similar approach. Yes, with small crutches. Yes, not within the entire system, but only in their projects (although if many penetrate, then it will gradually be pulled into libraries).
One thing: if there is a desire ... (Tahiti, Tahiti ... We were not in any Tahiti! They feed us well here.)