Manto em torno do ImmutableList em Java

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

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

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


All Articles