Por que o Java 8 não inclui coleções imutáveis?


130

A equipe do Java fez um grande trabalho removendo barreiras à programação funcional no Java 8. Em particular, as alterações nas coleções java.util fazem um ótimo trabalho de encadear transformações em operações de fluxo muito rápido. Considerando o bom trabalho que fizeram ao adicionar funções de primeira classe e métodos funcionais nas coleções, por que eles falharam completamente ao fornecer coleções imutáveis ​​ou até mesmo interfaces de coleção imutáveis?

Sem alterar nenhum código existente, a equipe Java pode, a qualquer momento, adicionar interfaces imutáveis ​​iguais às mutáveis, menos os métodos "set" e fazer com que as interfaces existentes se estendam a partir deles, assim:

                  ImmutableIterable
     ____________/       |
    /                    |
Iterable        ImmutableCollection
   |    _______/    /          \   \___________
   |   /           /            \              \
 Collection  ImmutableList  ImmutableSet  ImmutableMap  ...
    \  \  \_________|______________|__________   |
     \  \___________|____________  |          \  |
      \___________  |            \ |           \ |
                  List            Set           Map ...

Claro, operações como List.add () e Map.put () atualmente retornam um valor booleano ou anterior para a chave especificada para indicar se a operação foi bem-sucedida ou falhou. Coleções imutáveis ​​teriam que tratar métodos como fábricas e retornar uma nova coleção contendo o elemento adicionado - que é incompatível com a assinatura atual. Mas isso pode ser contornado usando um nome de método diferente, como ImmutableList.append () ou .addAt () e ImmutableMap.putEntry (). A verbosidade resultante seria mais do que compensada pelos benefícios de trabalhar com coleções imutáveis, e o sistema de tipos impediria erros de chamar o método errado. Com o tempo, os métodos antigos podem ser preteridos.

Vitórias de coleções imutáveis:

  • Simplicidade - o raciocínio sobre o código é mais simples quando os dados subjacentes não são alterados.
  • Documentação - se um método usa uma interface de coleção imutável, você sabe que não vai modificar essa coleção. Se um método retorna uma coleção imutável, você sabe que não pode modificá-lo.
  • Simultaneidade - coleções imutáveis ​​podem ser compartilhadas com segurança entre threads.

Como alguém que experimentou idiomas que assumem imutabilidade, é muito difícil voltar ao oeste selvagem de uma mutação desenfreada. As coleções de Clojure (abstração de sequência) já possuem tudo o que as coleções do Java 8 fornecem, além de imutabilidade (embora talvez usando memória e tempo extras devido a listas vinculadas sincronizadas em vez de fluxos). O Scala tem coleções mutáveis ​​e imutáveis ​​com um conjunto completo de operações e, embora essas operações sejam ágeis, chamar o .iterator oferece uma visão lenta (e existem outras maneiras de avaliá-las). Não vejo como o Java pode continuar competindo sem coleções imutáveis.

Alguém pode me apontar para a história ou discussão sobre isso? Certamente é público em algum lugar.


9
Relacionado a isso - Ayende blogou recentemente sobre coleções e coleções imutáveis ​​em C #, com benchmarks. ayende.com/blog/tags/performance - tl; dr - a imutabilidade é lenta .
Oded

20
com sua hierarquia posso dar-lhe uma ImmutableList e então alterá-lo em você quando você não espera que pode quebrar um monte de coisas, como é que você só tem constcoleções
catraca aberração

18
A imutabilidade @Oded é lenta, mas o bloqueio também. Assim é manter uma história. Simplicidade / correção vale velocidade em muitas situações. Com coleções pequenas, a velocidade não é um problema. A análise da Ayende é baseada na suposição de que você não precisa de histórico, bloqueio ou simplicidade e que está trabalhando com um grande conjunto de dados. Às vezes isso é verdade, mas não é uma coisa sempre é melhor. Existem trade-offs.
GlenPeterson

5
@GlenPeterson é para isso que servem as cópias defensivas Collections.unmodifiable*(). mas não os trate como imutáveis ​​quando não são
catraca aberração

13
Eh? Se sua função tiver um ImmutableListnesse diagrama, as pessoas podem passar um mutável List? Não, isso é uma violação muito ruim do LSP.
Telastyn

Respostas:


113

Porque coleções imutáveis ​​exigem absolutamente que o compartilhamento seja utilizável. Caso contrário, todas as operações soltarão uma lista totalmente diferente em algum lugar. Idiomas totalmente imutáveis, como Haskell, geram quantidades surpreendentes de lixo sem otimizações e compartilhamento agressivos. Ter uma coleção que só pode ser usada com <50 elementos não vale a pena colocar na biblioteca padrão.

Além disso, coleções imutáveis ​​geralmente têm implementações fundamentalmente diferentes das de suas contrapartes mutáveis. Considere, por exemplo ArrayList, um imutável eficiente ArrayListnão seria uma matriz! Ele deve ser implementado com uma árvore equilibrada com um grande fator de ramificação, Clojure usa 32 IIRC. Tornar coleções mutáveis ​​"imutáveis" ao adicionar apenas uma atualização funcional é um bug de desempenho tanto quanto um vazamento de memória.

Além disso, o compartilhamento não é viável em Java. Java fornece muitos ganchos irrestritos à mutabilidade e à igualdade de referência para tornar o compartilhamento "apenas uma otimização". Provavelmente seria um pouco irritante se você pudesse modificar um elemento em uma lista e percebesse que você acabou de modificar um elemento nas outras 20 versões da lista que você possui.

Isso também exclui enormes classes de otimizações vitais para imutabilidade eficiente, compartilhamento, fusão de fluxo, o que você quiser, a mutabilidade o interrompe. (Isso seria um bom slogan para os evangelistas da PF)


21
Meu exemplo falou sobre interfaces imutáveis . Java poderia fornecer um conjunto completo de implementações mutáveis ​​e imutáveis dessas interfaces que fariam as compensações necessárias. Cabe ao programador escolher mutável ou imutável, conforme apropriado. Os programadores precisam saber quando usar uma Lista vs. Definir agora. Geralmente, você não precisa da versão mutável até ter um problema de desempenho e, em seguida, pode ser necessário apenas como construtor. De qualquer forma, ter a interface imutável seria uma vitória por si só.
GlenPeterson

4
Li sua resposta novamente e acho que você está dizendo que o Java tem uma suposição fundamental de mutabilidade (por exemplo, java beans) e que as coleções são apenas a ponta do iceberg, e esculpir essa dica não resolverá o problema subjacente. Um ponto válido. Posso aceitar essa resposta e acelerar minha adoção do Scala! :-)
GlenPeterson

8
Não tenho certeza se coleções imutáveis ​​exigem a capacidade de compartilhar partes comuns para serem úteis. O tipo imutável mais comum em Java, uma coleção imutável de caracteres, usada para permitir o compartilhamento, mas não permite mais. A principal coisa que o torna útil é a capacidade de copiar rapidamente dados de um Stringpara um StringBuffer, manipulá-los e copiá-los para um novo imutável String. Usar esse padrão com conjuntos e listas pode ser tão bom quanto usar tipos imutáveis ​​que são projetados para facilitar a produção de instâncias ligeiramente alteradas, mas ainda podem ser melhores ...
supercat

3
É perfeitamente possível fazer uma coleção imutável em Java usando o compartilhamento. Os itens armazenados na coleção são referências e seus referentes podem ser alterados - e daí? Esse comportamento já quebra coleções existentes, como HashMap e TreeSet, mas elas são implementadas em Java. E se várias coleções contiverem referências ao mesmo objeto, é inteiramente esperado que a modificação do objeto cause uma alteração visível quando for visualizada em todas as coleções.
Segredo

4
jozefg, é perfeitamente possível implementar coleções imutáveis ​​eficientes na JVM com compartilhamento estrutural. Scala e Clojure os têm como parte de sua biblioteca padrão, ambas as implementações são baseadas no HAMT de Phil Bagwell (Hash Array Mapped Trie). Sua declaração sobre o Clojure implementando estruturas de dados imutáveis ​​com árvores BALANCED está completamente errada.
Sesm # 6/15

78

Uma coleção mutável não é um subtipo de uma coleção imutável. Em vez disso, coleções mutáveis ​​e imutáveis ​​são descendentes irmãos de coleções legíveis. Infelizmente, os conceitos de "legível", "somente leitura" e "imutável" parecem ficar confusos, apesar de terem três significados diferentes.

  • Uma classe base de coleção ou tipo de interface legível promete que se pode ler itens e não fornece nenhum meio direto de modificar a coleção, mas não garante que o código que recebe a referência não possa convertê-la ou manipulá-la de maneira a permitir a modificação.

  • Uma interface de coleção somente leitura não inclui nenhum membro novo, mas deve ser implementada apenas por uma classe que promete que não há como manipular uma referência a ela de forma a alterar a coleção nem receber uma referência a algo isso poderia fazê-lo. No entanto, não promete que a coleção não seja modificada por outra coisa que tenha uma referência aos internos. Observe que uma interface de coleção somente leitura pode não ser capaz de impedir a implementação por classes mutáveis, mas pode especificar que qualquer implementação ou classe derivada de uma implementação que permita a mutação seja considerada uma implementação "ilegítima" ou derivada de uma implementação .

  • Uma coleção imutável é aquela que sempre conterá os mesmos dados, desde que exista qualquer referência a ela. Qualquer implementação de uma interface imutável que nem sempre retorna os mesmos dados em resposta a uma solicitação específica é interrompida.

Às vezes é útil ter tipos fortemente associados mutáveis e imutáveis de cobrança que ambas implementam ou derivam do mesmo tipo "legível", e têm o tipo legível incluem AsImmutable, AsMutablee AsNewMutablemétodos. Esse design pode permitir que o código que deseja persistir os dados em uma coleção chame AsImmutable; esse método fará uma cópia defensiva se a coleção for mutável, mas pulará a cópia se já estiver imutável.


11
Ótima resposta. Coleções imutáveis ​​podem oferecer uma garantia bastante forte relacionada à segurança de threads e como você pode argumentar sobre elas com o passar do tempo. Uma coleção legível / somente leitura não. De fato, para honrar o princípio da subestação liskov, somente leitura e imutável provavelmente devem ser do tipo base abstrata com o método final e membros privados para garantir que nenhuma classe derivada possa destruir a garantia dada pelo tipo. Ou devem ser do tipo totalmente concreto que envolva uma coleção (somente leitura) ou sempre tira uma cópia defensiva (imutável). É assim que a ImmutableList da goiaba faz isso.
Laurent Bourgault-Roy,

11
@ LaurentBourgault-Roy: Existem vantagens para os tipos imutáveis ​​selados e herdáveis. Se alguém não deseja permitir que uma classe derivada ilegítima quebre seus invariantes, os tipos selados podem oferecer proteção contra isso, enquanto as classes herdáveis ​​não oferecem nenhuma. Por outro lado, pode ser possível que um código que saiba algo sobre os dados que ele possui armazene-o de maneira muito mais compacta do que um tipo que não sabe nada sobre ele. Considere, por exemplo, um tipo ReadableIndexedIntSequence que encapsula uma sequência de int, com métodos getLength()e getItemAt(int).
Super dec23

11
@ LaurentBourgault-Roy: Dado a ReadableIndexedIntSequence, poderia-se produzir uma instância de um tipo imutável suportado por matriz, copiando todos os itens em uma matriz, mas suponha que uma implementação específica simplesmente retornasse 16777216 para comprimento e ((long)index*index)>>24para cada item. Seria uma sequência imutável legítima de números inteiros, mas copiá-la para uma matriz seria um enorme desperdício de tempo e memória.
Supercat 23/12

11
Eu concordo plenamente. Minha solução fornece correção (até certo ponto), mas para obter desempenho com um grande conjunto de dados, você deve ter estrutura e design persistentes para imutabilidade desde o início. Para pequenas coleções, você pode tirar uma cópia imutável de tempos em tempos. Lembro-me de que Scala fez uma análise de vários programas e descobriu que 90% das listas instanciadas tinham 10 ou menos itens.
Laurent Bourgault-Roy,

11
@ LaurentBourgault-Roy: A questão fundamental é se alguém confia nas pessoas para não produzir implementações quebradas ou classes derivadas. Se houver, e se as interfaces / classes base fornecerem métodos asMutable / asImmutable, pode ser possível melhorar o desempenho em várias ordens de magnitude [por exemplo, compare o custo de chamar asImmutableuma instância da sequência acima definida versus o custo de construção uma cópia imutável baseada em array]. Eu diria que ter interfaces definidas para esses fins é provavelmente melhor do que tentar usar abordagens ad-hoc; IMHO, a maior razão ...
supercat

15

O Java Collections Framework fornece a capacidade de criar uma versão somente leitura de uma coleção por meio de seis métodos estáticos na classe java.util.Collections :

Como alguém apontou nos comentários da pergunta original, as coleções retornadas podem não ser consideradas imutáveis ​​porque, embora as coleções não possam ser modificadas (nenhum membro pode ser adicionado ou removido de uma coleção), os objetos reais referenciados pela coleção pode ser modificado se o tipo de objeto permitir.

No entanto, esse problema permaneceria independentemente de o código retornar um único objeto ou uma coleção de objetos não modificável. Se o tipo permitir que seus objetos sejam alterados, essa decisão foi tomada no design do tipo e não vejo como uma alteração no JCF poderia alterar isso. Se a imutabilidade é importante, os membros de uma coleção devem ser de um tipo imutável.


4
O design das coleções não modificáveis ​​teria sido bastante aprimorado se os invólucros incluíssem uma indicação de se a coisa a ser embrulhada já era imutável, e havia immutableListetc. métodos de fábrica que retornariam um invólucro somente leitura em torno de uma cópia de um pass-in lista , a menos que a lista passada já seja imutável . Seria fácil criar tipos definidos pelo usuário como esse, mas com um problema: não haveria maneira do joesCollections.immutableListmétodo reconhecer que não seria necessário copiar o objeto retornado por fredsCollections.immutableList.
Supercat

8

Esta é uma pergunta muito boa. Gosto de ter a ideia de que, de todo o código escrito em java e executado em milhões de computadores em todo o mundo, todos os dias, dia e noite, cerca de metade do total de ciclos de relógio deve ser desperdiçada sem fazer nada além de fazer cópias de segurança de coleções que são sendo retornado por funções. (E coleta de lixo dessas coleções milissegundos após sua criação.)

Uma porcentagem dos programadores de Java está ciente da existência da unmodifiableCollection()família de métodos da Collectionsclasse, mas mesmo entre eles, muitos simplesmente não se preocupam com isso.

E não posso culpá-los: uma interface que finge ser de leitura e gravação, mas gera um UnsupportedOperationExceptionse você cometer o erro de invocar qualquer um de seus métodos de 'gravação', é uma coisa muito ruim de se ter!

Agora, uma interface como a Collectionqual estaria faltando a add(), remove()e clear()métodos não seria uma interface "ImmutableCollection"; seria uma interface "UnmodifiableCollection". Por uma questão de fato, nunca poderia haver uma interface "ImmutableCollection", porque imutabilidade é uma natureza de uma implementação, não uma característica de uma interface. Eu sei, isso não é muito claro; deixe-me explicar.

Suponha que alguém lhe entregue uma interface de coleção somente leitura; é seguro passá-lo para outro segmento? Se você soubesse com certeza que isso representa uma coleção verdadeiramente imutável, a resposta seria "sim"; infelizmente, como é uma interface, você não sabe como é implementada; portanto, a resposta deve ser um não : pelo que você sabe, pode ser uma visão não modificável (para você) de uma coleção que é de fato mutável, (como o que você acompanha Collections.unmodifiableCollection()), ao tentar ler a partir dele enquanto outro segmento está modificando, resultaria na leitura de dados corrompidos.

Portanto, o que você descreveu essencialmente é um conjunto de interfaces de coleção não "Imutáveis", mas "Não Modificáveis". É importante entender que "Unmodifiable" significa simplesmente que quem tem uma referência a essa interface é impedido de modificar a coleção subjacente e eles são impedidos simplesmente porque a interface carece de métodos de modificação, não porque a coleção subjacente seja necessariamente imutável. A coleção subjacente pode muito bem ser mutável; você não tem conhecimento e não tem controle sobre isso.

Para ter coleções imutáveis, elas teriam que ser classes , não interfaces!

Essas classes de coleções imutáveis ​​teriam que ser finais, para que, quando você receber uma referência a essa coleção, saiba com certeza que ela se comportará como uma coleção imutável, independentemente do que você ou qualquer outra pessoa que faça referência a ela possa faça com isso.

Portanto, para ter um conjunto completo de coleções em java (ou qualquer outra linguagem imperativa declarativa), precisaríamos do seguinte:

  1. Um conjunto de interfaces de coleção não modificáveis .

  2. Um conjunto de interfaces de coleção mutáveis , estendendo as não modificáveis.

  3. Um conjunto de classes de coleção mutável implementando as interfaces mutáveis ​​e, por extensão, também as interfaces não modificáveis.

  4. Um conjunto de classes de coleção imutáveis , implementando interfaces não modificáveis, mas geralmente transmitidas como classes, para garantir a imutabilidade.

Eu implementei todas as opções acima por diversão, e as estou usando em projetos, e elas funcionam como um encanto.

A razão pela qual eles não fazem parte do tempo de execução do java é provavelmente porque se pensava que isso seria muito / muito complexo / muito difícil de entender.

Pessoalmente, acho que o que descrevi acima não é suficiente; mais uma coisa que parece ser necessário é um conjunto de interfaces mutáveis e classes para imutabilidade estrutural . (Que pode ser chamado simplesmente de "Rígido" porque o prefixo "Estruturalmente Imutável" é muito longo).


Bons pontos. Dois detalhes: 1. Coleções imutáveis ​​requerem determinadas assinaturas de métodos, especificamente (usando uma Lista como exemplo): List<T> add(T t)- todos os métodos "mutadores" devem retornar uma nova coleção que reflita a alteração. 2. Para o bem ou para o mal, as interfaces geralmente representam um contrato além de uma assinatura. Serializable é uma dessas interfaces. Da mesma forma, o Comparable exige que você implemente corretamente seu compareTo()método para funcionar corretamente e, idealmente, seja compatível com equals()e hashCode().
GlenPeterson

Ah, eu nem tinha em mente a imutabilidade das mutações por cópia. O que eu escrevi acima se refere a coleções imutáveis ​​simples e simples que realmente não têm métodos como esse add(). Mas suponho que se os métodos mutadores fossem adicionados às classes imutáveis, eles precisariam retornar também classes imutáveis. Portanto, se houver um problema oculto lá, eu não o vejo.
Mike Nakis

Sua implementação está disponível publicamente? Eu deveria ter perguntado isso meses atrás. De qualquer forma, a minha é: github.com/GlenKPeterson/UncleJim
GlenPeterson

4
Suppose someone hands you such a read-only collection interface; is it safe to pass it to another thread?Suponha que alguém passe uma instância de uma interface de coleção mutável. É seguro invocar algum método nele? Você não sabe que a implementação não se repete para sempre, gera uma exceção ou ignora completamente o contrato da interface. Por que ter um padrão duplo especificamente para coleções imutáveis?
Doval

11
IMHO seu raciocínio contra interfaces mutáveis ​​está errado. Você pode escrever uma implementação mutável de interfaces imutáveis ​​e depois ela será interrompida. Certo. Mas a culpa é sua, pois você está violando o contrato. Apenas pare de fazer isso. Não é diferente de quebrar uma SortedSetsubclasse do conjunto com uma implementação não conforme. Ou passando um inconsistente Comparable. Quase tudo pode ser quebrado, se você quiser. Eu acho que é isso que @Doval quis dizer com "padrões duplos".
Maaartinus 23/08

2

Coleções imutáveis ​​podem ser profundamente recursivas, comparadas umas com as outras, e não excessivamente ineficientes se a igualdade de objetos for por secureHash. Isso é chamado de floresta merkle. Pode ser por coleção ou em partes deles, como uma árvore AVL (binária com auto balanceamento) para um mapa classificado.

A menos que todos os objetos java nessas coleções tenham um ID exclusivo ou alguma cadeia de bits para hash, a coleção não terá nada para hash para nomear-se exclusivamente.

Exemplo: No meu laptop 4x1,6ghz, posso executar 200K sha256s por segundo do menor tamanho que se encaixa em 1 ciclo de hash (até 55 bytes), em comparação com 500K HashMap ops ou 3M ops em uma hashtable de longos. 200K / log (collectionSize) novas coleções por segundo são rápidas o suficiente para algumas coisas em que a integridade dos dados e a escalabilidade global anônima são importantes.


-3

Atuação. As coleções por natureza podem ser muito grandes. Copiar 1000 elementos para uma nova estrutura com 1001 elementos em vez de inserir um único elemento é simplesmente horrível.

Concorrência. Se você tiver vários threads em execução, poderá obter a versão atual da coleção e não a versão passada 12 horas atrás quando o thread foi iniciado.

Armazenamento. Com objetos imutáveis ​​em um ambiente multiencadeado, você pode acabar com dezenas de cópias do "mesmo" objeto em diferentes pontos do seu ciclo de vida. Não importa para um objeto Calendário ou Data, mas quando for uma coleção de 10.000 widgets, isso o matará.


12
Coleções imutáveis ​​somente requerem cópia se você não puder compartilhar por causa da mutabilidade generalizada como o Java. A simultaneidade geralmente é mais fácil com coleções imutáveis, porque elas não exigem bloqueio; e para obter visibilidade, você sempre pode ter uma referência mutável a uma coleção imutável (comum no OCaml). Com o compartilhamento, as atualizações podem ser essencialmente gratuitas. Você pode fazer alocações logaritmicamente mais do que com uma estrutura mutável, mas na atualização, muitos subobjetos expirados podem ser liberados imediatamente ou reutilizados, para que você não tenha necessariamente mais sobrecarga de memória.
perfil completo de Jon Purdy

4
Problemas de casal. As coleções em Clojure e Scala são imutáveis, mas suportam cópias leves. Adicionar elemento 1001 significa copiar menos de 33 elementos, além de fazer alguns novos ponteiros. Se você compartilhar uma coleção mutável entre threads, você terá todos os tipos de problemas de sincronização ao alterá-la. Operações como "remove ()" são um pesadelo. Além disso, coleções imutáveis ​​podem ser construídas de forma mutável e, em seguida, copiadas uma vez em uma versão imutável, segura para compartilhar entre threads.
GlenPeterson

4
Usar a simultaneidade como argumento contra a imutabilidade é incomum. Duplicatas também.
Tom Hawtin - tackline

4
Um pouco ofendido sobre os votos negativos aqui. O OP perguntou por que eles não implementaram coleções imutáveis ​​e, eu forneci uma resposta considerada para a pergunta. Presumivelmente, a única resposta aceitável entre os conscientes da moda é "porque eles cometeram um erro". Na verdade, tenho alguma experiência em refatorar grandes pedaços de código usando a classe BigDecimal, excelente por outro lado, puramente por causa do fraco desempenho devido à imutabilidade 512 vezes maior do que usar um double mais algumas bagunças para corrigir os decimais.
James Anderson

3
@ JamesAnderson: Meus problemas com a sua resposta: "Desempenho" - você pode dizer que coleções imutáveis ​​da vida real sempre implementam alguma forma de compartilhamento e reutilização para evitar exatamente o problema que você descreve. "Concorrência" - o argumento se resume a "Se você deseja mutabilidade, um objeto imutável não funciona". Quero dizer que, se houver uma noção de "versão mais recente da mesma coisa", algo precisará sofrer uma mutação, seja a própria coisa ou algo que a possua. E em "Armazenamento", você parece dizer que a mutabilidade às vezes não é desejada.
Jhominal
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.