Mais uma vez sobre o ImmutableList em Java

No meu artigo anterior, “ Cloaking around ImmutableList in Java ”, propus uma solução para o problema da ausência de listas imutáveis ​​em Java, que não é corrigido , nem agora nem nunca , em Java.


A solução foi elaborada apenas no nível de “existe uma ideia” e a implementação no código foi torta; portanto, tudo foi percebido um pouco cético. Neste artigo, proponho uma solução modificada. A lógica de uso e a API são trazidas para um nível aceitável. A implementação no código está no nível beta.


Declaração do problema


Usaremos as definições do artigo original. Em particular, isso significa que ImmutableList é uma lista imutável de referências a alguns objetos. Se esses objetos não forem imutáveis, a lista também não será um objeto imutável, apesar do nome. Na prática, é improvável que isso prejudique alguém, mas, para evitar expectativas injustificadas, é necessário mencionar.


Também está claro que a imutabilidade da lista pode ser "hackeada" por meio de reflexões ou criando suas próprias classes no mesmo pacote, seguido de subir nos campos protegidos da lista, ou algo semelhante.


Ao contrário do artigo original, não vamos aderir ao princípio de "tudo ou nada": o autor parece acreditar que se o problema não puder ser resolvido no nível do JDK, nada deverá ser feito. (Na verdade, há outra pergunta: "não pode ser resolvida" ou "os autores de Java não desejavam resolvê-la". Parece-me que ainda seria possível adicionando interfaces, classes e métodos adicionais para aproximar as coleções existentes. aparência desejada, embora menos bonita do que se você tivesse pensado nisso imediatamente, mas agora não é sobre isso.)


Criaremos uma biblioteca que pode coexistir com sucesso com coleções existentes em Java.


As principais idéias da biblioteca:


  • Existem MutableList ImmutableList e MutableList . Ao transmitir tipos, é impossível obter um do outro.
  • Em nosso projeto, que queremos melhorar usando a biblioteca, substituímos todas as List s por uma dessas duas interfaces. Se em algum momento você não puder ficar sem a List , na primeira oportunidade, converteremos a List de / para uma das duas interfaces. O mesmo se aplica aos momentos de recebimento / transmissão de dados para bibliotecas de terceiros usando a List .
  • Conversões mútuas entre ImmutableList , MutableList , List devem ser executadas o mais rápido possível (ou seja, sem copiar listas, se possível). Sem conversões de ida e volta "baratas", toda a idéia começa a parecer duvidosa.

Note-se que apenas as listas são consideradas, pois no momento somente elas são implementadas na biblioteca. Mas nada impede que a biblioteca complemente com Set e Map s.


API


ImmutableList


ImmutableList é o sucessor de ReadOnlyList (que, como no artigo anterior, é uma interface de List copiada da qual todos os métodos de mutação são lançados). Métodos adicionados:


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

O método toList fornece a capacidade de transmitir um ImmutableList para partes de código que aguardam uma List . Um wrapper é retornado no qual todos os métodos de modificação retornam UnsupportedOperationException e os métodos restantes são redirecionados para o ImmutableList original.


O método mutable converte um ImmutableList em um MutableList . Um wrapper é retornado no qual todos os métodos são redirecionados para o ImmutableList original até a primeira alteração. Antes da alteração, o wrapper é desatado do ImmutableList original, copiando seu conteúdo para o ArrayList interno, para o qual todas as operações são redirecionadas.


O método contentEquals destina-se a comparar o conteúdo da lista com o conteúdo de uma Iterable arbitrária passada (é claro, essa operação é significativa apenas para as implementações Iterable que possuem uma ordem distinta de elementos).


Observe que em nossa implementação do ReadOnlyList , os listIterator iterator e listIterator retornam o padrão java.util.Iterator / java.util.ListIterator . Esses iteradores contêm métodos de modificação que precisarão ser suprimidos lançando uma UnsupportedOperationException . Seria preferível criar nosso ReadOnlyIterator , mas nesse caso não pudemos escrever for (Object item : immutableList) , o que estragaria imediatamente todo o prazer de usar a biblioteca.


MutableList


MutableList é o descendente da List regular. Métodos adicionados:


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

O método de snapshot foi projetado para obter um "instantâneo" do estado atual de MutableList como um ImmutableList . O "instantâneo" é salvo dentro do MutableList e, se o estado não tiver sido alterado no momento da próxima chamada de método, a mesma instância do ImmutableList . O "instantâneo" armazenado dentro é descartado na primeira vez que qualquer método de modificação é chamado ou quando releaseSnapshot chamado. O método releaseSnapshot pode ser usado para economizar memória, se você tiver certeza de que ninguém precisará de um "instantâneo", mas os métodos de modificação não serão chamados em breve.


Mutabor


A classe Mutabor fornece um conjunto de métodos estáticos que são os "pontos de entrada" para a biblioteca.


Sim, o projeto agora é chamado de "mutabor" (é consoante com "mutável", e na tradução significa "eu vou transformar", o que está de acordo com a idéia de "transformar" rapidamente alguns tipos de coleções em outros).


 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* projetados para criar coleções apropriadas, copiando os dados fornecidos. Os métodos convertTo* uma conversão rápida da coleção transferida para o tipo desejado e, se não foi possível converter rapidamente, eles executam cópias lentas. Se a conversão rápida foi bem-sucedida, a coleção original é limpa e supõe-se que ela não será usada no futuro (embora possa, mas isso dificilmente faz sentido).


As chamadas para os construtores dos ImmutableList implementação ImmutableList / MutableList ocultas. Supõe-se que o usuário lide apenas com interfaces, ele não cria esses objetos e usa os métodos descritos acima para transformar coleções.


Detalhes da implementação


ImmutableListImpl


Encapsula uma matriz de objetos. A implementação corresponde aproximadamente à implementação ArrayList , da qual todos os métodos de modificação e verificações de modificação simultânea são lançados.


A implementação dos contentEquals toList e contentEquals também contentEquals bastante trivial. O método toList retorna um wrapper que redireciona as chamadas para um determinado ImmutableList ; a cópia lenta dos dados não ocorre.


O método mutable retorna um MutableListImpl criado com base neste ImmutableList . A cópia de dados não ocorre até que qualquer método de modificação seja chamado no MutableList recebido.


MutableListImpl


Encapsula links para ImmutableList e List . Ao criar um objeto, apenas um desses dois links é sempre preenchido, o outro permanece null .


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

Os métodos imutáveis ​​redirecionam as chamadas para ImmutableList se não for null , e para List caso contrário.


Os métodos de modificação redirecionam as chamadas para a List , após a inicialização:


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

O método de snapshot fica assim:


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

A implementação dos contentEquals releaseSnapshot e contentEquals trivial.


Essa abordagem permite minimizar o número de cópias de dados durante o uso "comum", substituindo as cópias por conversões rápidas.


Conversão rápida de lista


Conversões rápidas são possíveis para as classes ArrayList ou Arrays$ArrayList (o resultado do método Arrays.asList() ). Na prática, na grande maioria dos casos, são precisamente essas classes que se deparam.


Dentro dessas classes, há uma matriz de elementos. A essência de uma conversão rápida é obter uma referência a essa matriz por meio de reflexões (este é um campo privado) e substituí-la por uma referência a uma matriz vazia. Isso garante que a única referência à matriz permaneça com o nosso objeto, e a matriz permaneça inalterada.


Na versão anterior da biblioteca, conversões rápidas de tipos de coleção eram realizadas chamando o construtor. Ao mesmo tempo, o objeto de coleção original se deteriorou (tornou-se inadequado para uso posterior), o que você não espera inconscientemente do designer. Agora, um método estático especial é usado para a conversão, e a coleção original não estraga, mas é simplesmente limpa. Assim, um comportamento incomum assustador foi eliminado.


Problemas com equals / hashCode


As coleções Java usam uma abordagem muito estranha para implementar métodos equals e hashCode .


A comparação é realizada de acordo com o conteúdo, que parece lógico, mas a classe da lista em si não é levada em consideração. Portanto, por exemplo, ArrayList e LinkedList com o mesmo conteúdo serão equals .


Aqui está a implementação equals / hashCode de AbstractList (da qual ArrayList é herdada)
 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; } 

Portanto, agora é absolutamente necessário que todas as implementações da List tenham uma implementação equals (e, como resultado, hashCode ). Caso contrário, você poderá obter situações em que a.equals(b) && !b.equals(a) , o que não é bom. Uma situação semelhante é com Set e Map .


Quando aplicado à biblioteca, isso significa que a implementação de equals e hashCode para MutableList predefinida e, em tal implementação, ImmutableList e MutableList com o mesmo conteúdo não podem ser equals (já que ImmutableList não ImmutableList uma List ). Portanto, os métodos contentEquals foram adicionados para comparar o conteúdo.


A implementação dos métodos equals e hashCode para ImmutableList feita completamente semelhante à versão de AbstractList , mas com a substituição de List por ReadOnlyList .


Total


As fontes e os testes da biblioteca são postados por referência na forma de um projeto maven.


Caso alguém queira usar a biblioteca, ele criou um grupo em contato para receber "feedback".


O uso da biblioteca é bastante óbvio, aqui está um pequeno exemplo:


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

Boa sorte a todos! Envie relatórios de erros!

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


All Articles