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) {
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; }
Boa sorte a todos! Envie relatórios de erros!