Capa alrededor de ImmutableList en Java

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) { //   ArrayList,      -  arr = original.toArray(); } this.data = arr; } 

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í).

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


All Articles