Leí el artículo "No habrá colecciones inmutables en Java, ni ahora ni nunca", y pensé que el problema de la ausencia de listas inmutables en Java, lo que entristece al autor, es bastante solucionable en una escala limitada. Ofrezco mis pensamientos y piezas de código sobre este tema.
(Este es un artículo de respuesta, lea primero el artículo original).
InmodificableList vs ImmutableList
La primera pregunta que surge es: ¿por qué necesito una UnmodifiableList
, si hay una ImmutableList
? Como resultado de la discusión, se ven dos ideas con respecto al significado de UnmodifiableList
en los comentarios del artículo original:
- el método recibe una
UnmodifiableList
, no puede cambiarlo por sí mismo, pero sabe que el contenido puede ser cambiado por otro hilo (y sabe cómo manejarlo correctamente) - otros hilos no afectan,
UnmodifiableList
e ImmutableList
son equivalentes para un método, pero UnmodifiableList
usa como uno más "ligero".
La primera opción parece demasiado rara en la práctica. Por lo tanto, si puede hacer una implementación "fácil" de ImmutableList
, entonces UnmodifiableList
no será muy necesario. Por lo tanto, en el futuro lo olvidaremos y solo implementaremos ImmutableList
.
Declaración del problema.
Implementaremos la opción ImmutableList
:
- La API debe ser idéntica a la API de
List
normal en la parte de "lectura". La parte de "escritura" debe estar ausente. ImmutableList
y List
no deberían estar relacionadas por relaciones de herencia. ¿Por qué? - entiende el artículo original.- Tiene sentido hacer la implementación por analogía con
ArrayList
. Esta es la opción más fácil. - La implementación debe evitar copiar matrices siempre que sea posible.
Implementación de ImmutableList
Primero, tratamos con la API. Examinamos las interfaces de Collection
y List
y copiamos la parte de "lectura" de ellas en nuestras nuevas 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); }
A continuación, cree la clase ImmutableList
. La firma es similar a ArrayList
(pero implementa la interfaz ReadOnlyList
en lugar de List
).
public class ImmutableList<E> implements ReadOnlyList<E>, RandomAccess, Cloneable, Serializable
Copiamos la implementación de la clase de ArrayList
y refaccionamos firmemente, desechando todo lo relacionado con la parte de "escritura", comprobando la modificación concurrente, etc.
Los constructores serán los siguientes:
public ImmutableList() public ImmutableList(E[] original) public ImmutableList(Collection<? extends E> original)
El primero crea una lista vacía. El segundo crea una lista copiando la matriz. No podemos prescindir de la copia si queremos lograr que sea inmutable. El tercero es más interesante. Un constructor de ArrayList
similar también copia datos de la colección. Haremos lo mismo, a menos que orginal
sea una instancia de ArrayList
o Arrays$ArrayList
(esto es lo que devuelve el método Arrays.asList()
). Podemos suponer con seguridad que estos casos cubrirán el 90% de las llamadas del constructor.
En estos casos, "robaremos" la matriz original
través de reflexiones (hay esperanza de que esto sea más rápido que copiar matrices de gigabytes). La esencia del "robo":
- llegamos al campo privado
original
, que almacena la matriz ( ArrayList.elementData
) - copiar el enlace a la matriz a nosotros mismos
- poner en el campo fuente nulo
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) {
Como contrato, suponemos que cuando se llama al constructor, la lista mutable se ImmutableList
en una ImmutableList
. La lista original no se puede usar después de eso. Al intentar usar, llega una NullPointerException
. Esto asegura que la matriz "robada" no cambiará y nuestra lista será realmente inmutable (excepto en el caso de que alguien llegue a la matriz a través de reflexiones).
Otras clases
Supongamos que decidimos usar una ImmutableList
en un proyecto real.
El proyecto interactúa con las bibliotecas: recibe de ellas y les envía varias listas. En la gran mayoría de los casos, estas listas serán una ArrayList
. La implementación descrita de ImmutableList
permite convertir rápidamente la ArrayList
resultante en una ImmutableList
. También es necesario implementar la conversión para las listas enviadas a las bibliotecas: ImmutableList
to List
. Para una conversión rápida, necesita un contenedor ImmutableList
que implemente List
, lanzando excepciones al intentar escribir en la lista (similar a Collections.unmodifiableList
).
Además, el proyecto mismo de alguna manera procesa las listas. Tiene sentido crear una clase MutableList
que represente una lista mutable, con una implementación basada en ArrayList
. En este caso, puede refactorizar el proyecto sustituyendo por todo ArrayList
clase que declara explícitamente la intención: MutableList
o MutableList
.
Necesita una conversión rápida de MutableList
a MutableList
y viceversa. Al mismo tiempo, a diferencia de la conversión de ArrayList
a ImmutableList
, ya no podemos estropear la lista original.
La conversión allí generalmente será lenta, con la copia de la matriz. Pero para los casos en que la MutableList
recibida no siempre cambia, puede hacer un contenedor: MutableList
, que guarda una referencia a ImmutableList
y la usa para métodos de "lectura", y si se llama a un método de "escritura", solo se olvida de ImmutableList
, después de copiar su contenido matriz para sí mismo, y luego ya funciona con su matriz (algo remotamente similar está en CopyOnWriteArrayList
).
Convertir "atrás" implica recibir una instantánea del contenido de MutableList
en el momento en que se llama al método. Nuevamente, en la mayoría de los casos no puede prescindir de copiar una matriz, pero puede hacer un contenedor para optimizar los casos de varias conversiones entre las cuales el contenido de MutableList
no cambió. Otra opción para convertir "atrás": algunos datos se recopilan en MutableList
, y cuando se completa la recopilación de datos, MutableList
debe convertirse para siempre en una ImmutableList
. También se implementa sin problemas con otro contenedor.
Total
Los resultados del experimento en forma de código se publican aquí.
Se ImmutableList
sí, que se describe en la sección "Otras clases" (¿todavía no?).
Podemos suponer que la premisa del artículo original, "colecciones inmutables en Java no será" es errónea.
Si hay un deseo, entonces es muy posible utilizar un enfoque similar. Sí, con muletas pequeñas. Sí, no dentro de todo el sistema, sino solo en sus proyectos (aunque si muchos penetran, se irá incorporando gradualmente a las bibliotecas).
Una cosa: si hay un deseo ... (Tahití, Tahití ... ¡No estábamos en ningún Tahití! Nos alimentan bien aquí).