Una vez más sobre ImmutableList en Java

En mi artículo anterior, " Ocultando ImmutableList en Java ", propuse una solución al problema de la ausencia de listas inmutables en Java, que no está solucionado , ni ahora ni nunca , en Java.


La solución se resolvió solo al nivel de "existe una idea así", y la implementación en el código fue torcida, por lo tanto, todo se percibió un tanto escéptico. En este artículo, propongo una solución modificada. La lógica de uso y la API se llevan a un nivel aceptable. La implementación en el código es hasta el nivel beta.


Declaración del problema.


Utilizaremos las definiciones del artículo original. En particular, esto significa que ImmutableList es una lista inmutable de referencias a algunos objetos. Si estos objetos resultan no ser inmutables, la lista tampoco será un objeto inmutable, a pesar del nombre. En la práctica, es poco probable que esto perjudique a nadie, pero para evitar expectativas injustificadas es necesario mencionarlo.


También está claro que la inmutabilidad de la lista puede ser "pirateada" por medio de reflexiones, o creando sus propias clases en el mismo paquete, seguido de subir a los campos protegidos de la lista, o algo similar.


A diferencia del artículo original, no nos adheriremos al principio de "todo o nada": el autor parece creer que si el problema no se puede resolver al nivel JDK, entonces no se debe hacer nada. (En realidad, otra pregunta, "no se puede resolver" o "los autores de Java no tenían el deseo de resolverlo". Me parece que aún sería posible al agregar interfaces, clases y métodos adicionales para acercar las colecciones existentes a aspecto deseado, aunque menos hermoso que si lo hubiera pensado de inmediato, pero ahora no se trata de eso).


Crearemos una biblioteca que pueda coexistir con éxito con las colecciones existentes en Java.


Las ideas principales de la biblioteca:


  • Hay MutableList y MutableList . Al lanzar tipos es imposible obtener uno del otro.
  • En nuestro proyecto, que queremos mejorar usando la biblioteca, reemplazamos todas las List con una de estas dos interfaces. Si en algún momento no puede prescindir de la List , en la primera oportunidad convertiremos la List de / a una de las dos interfaces. Lo mismo se aplica a los momentos de recepción / transmisión de datos a bibliotecas de terceros mediante List .
  • Las conversiones mutuas entre MutableList , MutableList , List deben realizarse lo más rápido posible (es decir, sin copiar listas, si es posible). Sin conversiones "baratas" de ida y vuelta, toda la idea comienza a parecer dudosa.

Cabe señalar que solo se consideran listas, ya que por el momento solo se implementan en la biblioteca. Pero nada impide que la biblioteca se complemente con Set y Map s.


API


Lista inmutable


ImmutableList es el sucesor de ReadOnlyList (que, como en el artículo anterior, es una interfaz de List copiada desde la cual se lanzan todos los métodos de mutación). Métodos añadidos:


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

El método toList proporciona la capacidad de pasar una ImmutableList a fragmentos de código que esperan una List . Se devuelve un contenedor en el que todos los métodos de modificación devuelven una UnsupportedOperationException , y los métodos restantes se redirigen a la ImmutableList original.


El método mutable convierte una MutableList una MutableList . Se devuelve un contenedor en el que todos los métodos se redirigen a la ImmutableList original hasta el primer cambio. Antes del cambio, el contenedor se desata de la ImmutableList original, copiando su contenido a la ArrayList interna, a la que se redirigen todas las operaciones.


El método contentEquals destinado a comparar el contenido de la lista con el contenido de un Iterable arbitrario pasado (por supuesto, esta operación es significativa solo para aquellas implementaciones Iterable que tienen un orden distinto de elementos).


Tenga en cuenta que en nuestra implementación de ReadOnlyList , los listIterator y listIterator devuelven java.util.Iterator / java.util.ListIterator estándar. Estos iteradores contienen métodos de modificación que deberán suprimirse lanzando una UnsupportedOperationException . Sería preferible hacer nuestro ReadOnlyIterator , pero en este caso no podríamos escribir for (Object item : immutableList) , lo que arruinaría de inmediato todo el placer de usar la biblioteca.


MutableList


MutableList es el descendiente de la List regular. Métodos añadidos:


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

El método de snapshot está diseñado para obtener una "instantánea" del estado actual de MutableList como MutableList . La "instantánea" se guarda dentro de MutableList , y si el estado no ha cambiado en el momento de la siguiente llamada al método, ImmutableList la misma instancia de ImmutableList . La "instantánea" almacenada en su interior se descarta la primera vez que se llama a cualquier método de modificación, o cuando releaseSnapshot llama a releaseSnapshot . El método releaseSnapshot se puede usar para ahorrar memoria si está seguro de que nadie necesitará una "instantánea", pero los métodos de modificación no se llamarán pronto.


Mutabor


La clase Mutabor proporciona un conjunto de métodos estáticos que son los "puntos de entrada" a la biblioteca.


Sí, el proyecto ahora se llama "mutabor" (está en consonancia con "mutable" y, en traducción, significa "voy a transformar", lo que concuerda con la idea de "transformar" rápidamente algunos tipos de colecciones en otros).


 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* métodos copyTo* diseñados para crear colecciones apropiadas copiando los datos proporcionados. Los métodos convertTo* una conversión rápida de la colección transferida al tipo deseado, y si no fue posible convertir rápidamente, realizan una copia lenta. Si la conversión rápida fue exitosa, la colección original se borra y se supone que no se usará en el futuro (aunque puede, pero esto no tiene sentido).


Las llamadas a los constructores de los ImmutableList implementación MutableList / MutableList ocultas. Se supone que el usuario solo trata con interfaces, no crea tales objetos y usa los métodos descritos anteriormente para transformar colecciones.


Detalles de implementación


ImmutableListImpl


Encapsula una matriz de objetos. La implementación corresponde aproximadamente a la implementación de ArrayList , desde la cual se lanzan todos los métodos de modificación y comprobaciones de modificaciones concurrentes.


La implementación de los contentEquals toList y contentEquals también contentEquals bastante trivial. El método toList devuelve un contenedor que redirige las llamadas a una ImmutableList determinada; no se produce una copia lenta de los datos.


El método mutable devuelve un MutableListImpl creado en base a esta ImmutableList . La copia de datos no se produce hasta que se llama a cualquier método de modificación en la MutableList recibida.


MutableListImpl


Encapsula enlaces a ImmutableList y List . Al crear un objeto, solo uno de estos dos enlaces siempre se llena, el otro permanece null .


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

Los métodos inmutables redirigen las llamadas a ImmutableList si no es null , y a List contrario.


Los métodos de modificación redirigen las llamadas a la List , después de inicializar:


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

El método de la snapshot se ve así:


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

La implementación de los contentEquals releaseSnapshot y contentEquals trivial.


Este enfoque le permite minimizar la cantidad de copias de datos durante el uso "ordinario", reemplazando copias con conversiones rápidas.


Conversión de lista rápida


Las conversiones rápidas son posibles para las clases ArrayList o Arrays$ArrayList (el resultado del método Arrays.asList() ). En la práctica, en la gran mayoría de los casos, son precisamente estas clases las que se encuentran.


Dentro de estas clases contienen una serie de elementos. La esencia de una conversión rápida es obtener una referencia a esta matriz a través de reflexiones (este es un campo privado) y reemplazarla con una referencia a una matriz vacía. Esto asegura que la única referencia a la matriz permanezca con nuestro objeto, y la matriz permanezca sin cambios.


En la versión anterior de la biblioteca, se realizaron conversiones rápidas de los tipos de colección llamando al constructor. Al mismo tiempo, el objeto de colección original se deterioró (se volvió inadecuado para su uso posterior), que inconscientemente no espera del diseñador. Ahora, se utiliza un método estático especial para la conversión, y la colección original no se estropea, sino que simplemente se borra. Por lo tanto, se eliminó el comportamiento inusual aterrador.


Problemas con equals / hashCode


Las colecciones Java utilizan un enfoque muy extraño para implementar métodos equals y hashCode .


La comparación se lleva a cabo de acuerdo con el contenido, que parece ser lógico, pero la clase de la lista en sí misma no se tiene en cuenta. Por lo tanto, por ejemplo, ArrayList y LinkedList con el mismo contenido serán equals .


Aquí está la implementación equals / hashCode de AbstractList (de la cual se hereda ArrayList)
 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; } 

Por lo tanto, ahora se requiere que todas las implementaciones de List tengan una implementación equals (y, como resultado, hashCode ). De lo contrario, puede obtener situaciones en las que a.equals(b) && !b.equals(a) , lo cual no es bueno. Una situación similar es con Set y Map .


En la aplicación a la biblioteca, esto significa que la implementación de equals y hashCode para MutableList predefinida, y en dicha implementación, MutableList y MutableList con el mismo contenido no pueden ser equals (ya que ImmutableList no ImmutableList una List ). Por lo tanto, se han agregado métodos contentEquals para comparar contenido.


La implementación de los métodos equals y hashCode para hashCode ImmutableList hace completamente similar a la versión de AbstractList , pero con el reemplazo de List por ReadOnlyList .


Total


Las fuentes y pruebas de la biblioteca se publican por referencia en forma de un proyecto Maven.


En caso de que alguien quiera usar la biblioteca, ha creado un grupo en contacto para "comentarios".


Usar la biblioteca es bastante obvio, aquí hay un breve ejemplo:


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

¡Buena suerte a todos! Enviar informes de errores!

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


All Articles