Li o artigo “Não haverá coleções imutáveis em Java - nem agora nem nunca” e pensei que o problema da ausência de listas imutáveis em Java, o que deixa o autor triste, é bastante solucionável em uma escala limitada. Eu ofereço meus pensamentos e partes de código sobre esse assunto.
(Este é um artigo de resposta, leia o artigo original primeiro.)
UnmodifiableList vs ImmutableList
A primeira pergunta que surge é: por que eu preciso de uma UnmodifiableList
, se houver uma ImmutableList
? Como resultado da discussão, duas idéias sobre o significado de UnmodifiableList
são vistas nos comentários do artigo original:
- o método recebe um
UnmodifiableList
, não pode alterá-lo, mas sabe que o conteúdo pode ser alterado por outro thread (e sabe como lidar com isso corretamente) - outros threads não afetam,
UnmodifiableList
e ImmutableList
são equivalentes a um método, mas UnmodifiableList
usado como um método mais "leve".
A primeira opção parece muito rara na prática. Portanto, se você puder fazer uma implementação "fácil" do ImmutableList
, o UnmodifiableList
se tornará pouco necessário. Portanto, no futuro, esqueceremos isso e implementaremos apenas o ImmutableList
.
Declaração do problema
Implementaremos a opção ImmutableList
:
- A API deve ser idêntica à API de
List
comum na parte "leitura". A parte "escrita" deve estar ausente. ImmutableList
e List
não devem ser relacionados por relações de herança. Por que isso - entende o artigo original.- Faz sentido fazer a implementação por analogia com
ArrayList
. Esta é a opção mais fácil. - A implementação deve evitar copiar matrizes sempre que possível.
Implementação ImmutableList
Primeiro, lidamos com a API. Examinamos as interfaces Collection
e List
e copiamos a parte "leitura" delas para nossas novas 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); }
Em seguida, crie a classe ImmutableList
. A assinatura é semelhante a ArrayList
(mas implementa a interface ReadOnlyList
vez de List
).
public class ImmutableList<E> implements ReadOnlyList<E>, RandomAccess, Cloneable, Serializable
Copiamos a implementação da classe de ArrayList
e refatoramos com firmeza, descartando tudo relacionado à parte "escrita", verificando a modificação simultânea etc.
Os construtores serão os seguintes:
public ImmutableList() public ImmutableList(E[] original) public ImmutableList(Collection<? extends E> original)
O primeiro cria uma lista vazia. O segundo cria uma lista, copiando a matriz. Não podemos ficar sem copiar se queremos alcançar imutáveis. O terceiro é mais interessante. Um construtor ArrayList
semelhante também copia dados da coleção. Faremos o mesmo, a menos que orginal
seja uma instância de ArrayList
ou Arrays$ArrayList
(é o que é retornado pelo método Arrays.asList()
). Podemos assumir com segurança que esses casos cobrirão 90% das chamadas do construtor.
Nesses casos, "roubaremos" a matriz original
por meio de reflexões (há esperança de que isso seja mais rápido do que copiar matrizes de gigabytes). A essência do "roubo":
- chegamos ao campo privado
original
, que armazena a matriz ( ArrayList.elementData
) - copie o link para a matriz para nós mesmos
- colocar no campo de origem null
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, assumimos que, quando o construtor é chamado, a lista mutável é ImmutableList
em um ImmutableList
. A lista original não pode ser usada depois disso. Ao tentar usar, um NullPointerException
chega. Isso garante que a matriz "roubada" não seja alterada e nossa lista seja realmente imutável (exceto no caso em que alguém chegue à matriz através de reflexões).
Outras classes
Suponha que decidamos usar um ImmutableList
em um projeto real.
O projeto interage com as bibliotecas: recebe e envia várias listas. Na grande maioria dos casos, essas listas serão um ArrayList
. A implementação descrita de ImmutableList
permite converter rapidamente o ArrayList
resultante em um ImmutableList
. Também é necessário implementar a conversão para listas enviadas para bibliotecas: ImmutableList
to List
. Para uma conversão rápida, você precisa de um invólucro ImmutableList
que implemente List
, lançando exceções ao tentar gravar na lista (semelhante a Collections.unmodifiableList
).
Além disso, o próprio projeto processa as listas de alguma forma. Faz sentido criar uma classe MutableList
que represente uma lista mutável, com uma implementação baseada em ArrayList
. Nesse caso, você pode refatorar o projeto substituindo todos os ArrayList
classe que declare explicitamente a intenção: ImmutableList
ou MutableList
.
Precisa de uma conversão rápida de ImmutableList
para MutableList
e vice-versa. Ao mesmo tempo, diferentemente da conversão de ArrayList
em ImmutableList
, não podemos mais estragar a lista original.
A conversão normalmente será lenta, com a cópia da matriz. Mas, nos casos em que o MutableList
recebido nem sempre muda, é possível criar um wrapper: MutableList
, que salva uma referência ao ImmutableList
e o usa para métodos de "leitura" e, se um método de "gravação" é chamado, esquece-se do ImmutableList
depois de copiar seu conteúdo array para si mesmo e, em seguida, ele já funciona com seu array (algo remotamente semelhante está em CopyOnWriteArrayList
).
A conversão de "volta" implica receber uma captura instantânea do conteúdo da MutableList
no momento em que o método é chamado. Novamente, na maioria dos casos, você não pode fazer isso sem copiar uma matriz, mas pode criar um wrapper para otimizar os casos de várias conversões entre as quais o conteúdo do MutableList
não foi alterado. Outra opção para converter "back": alguns dados são coletados no MutableList
e, quando a coleta de dados é concluída, o MutableList
precisa ser convertido para sempre em um ImmutableList
. Também é implementado sem problemas com outro wrapper.
Total
Os resultados do experimento na forma de código são publicados aqui
ImmutableList
próprio ImmutableList
é ImmutableList
, descrito na seção "Outras classes" (ainda não?).
Podemos assumir que a premissa do artigo original, “coleções imutáveis em Java não serão” é incorreta.
Se você quiser, é bem possível usar uma abordagem semelhante. Sim, com pequenas muletas. Sim, não dentro de todo o sistema, mas apenas em seus projetos (embora se muitos penetrem, ele será gradualmente puxado para as bibliotecas).
Uma coisa: se houver um desejo ... (Taiti, Taiti ... Nós não estávamos em nenhum Taiti! Eles nos alimentam bem aqui.)