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) {
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; }
¡Buena suerte a todos! Enviar informes de errores!