Não haverá coleções imutáveis ​​em Java - nem agora nem nunca

Olá pessoal!

Hoje, sua atenção é convidada para a tradução de um artigo cuidadosamente escrito sobre um dos problemas básicos do Java - a mutabilidade e como isso afeta a estrutura das estruturas de dados e o trabalho com eles. O material foi retirado do blog de Nicolai Parlog, cujo brilhante estilo literário realmente tentamos manter em tradução. O próprio Nicholas é notavelmente caracterizado por um trecho do blog da empresa JUG.ru em Habré; vamos citar esta passagem em sua totalidade:


Nikolay Parlog é um cara da mídia que faz análises sobre os recursos Java. Mas ele não é da Oracle ao mesmo tempo, então as avaliações são surpreendentemente francas e compreensíveis. Às vezes, depois deles, alguém é demitido, mas raramente. Nikolay irá falar sobre o futuro do Java, o que estará na nova versão. Ele é bom em falar sobre tendências e geralmente sobre o grande mundo. Ele é um companheiro muito lido e erudito. Até relatórios simples são agradáveis ​​de ouvir, o tempo todo que você aprende algo novo. Além disso, Nicholas sabe além do que está dizendo. Ou seja, você pode acessar qualquer relatório e apenas apreciá-lo, mesmo que esse não seja o seu tópico. Ele está ensinando. Ele escreveu "O Java Module System" para a Manning Publishing House, mantém blogs sobre desenvolvimento de software no codefx.org e está envolvido há vários projetos de código aberto. Ele pode ser contratado na conferência, é freelancer. É verdade que um freelancer muito caro. Aqui está o relatório .

Nós lemos e votamos. Quem gosta especialmente da postagem - também recomendamos que você analise os comentários do leitor na postagem original.

Volatilidade é ruim, certo? Assim, a imutabilidade é boa . As principais estruturas de dados nas quais a imutabilidade é especialmente proveitosa são as coleções: em Java, é uma lista ( List ), um conjunto ( Set ) e um dicionário ( Map ). No entanto, embora o JDK venha com coleções imutáveis ​​(ou não modificáveis?), O sistema de tipos não sabe nada sobre isso. Não há ImmutableList no JDK, e esse tipo de Guava parece completamente inútil para mim. Mas porque? Por que não adicionar Immutable... a essa mistura e não dizer que deveria ser?

O que é uma coleção imutável?


Na terminologia do JDK, os significados das palavras " imutável " e " não modificável " mudaram nos últimos anos. Inicialmente, “não modificável” era chamada de instância que não permitia mutabilidade (mutabilidade): em resposta à mudança de métodos, ele lançou UnsupportedOperationException . No entanto, poderia ser alterado de outra maneira - talvez porque fosse apenas um invólucro em torno de uma coleção mutável. Essas visualizações são refletidas nos métodos Collections::unmodifiableList , unmodifiableSet e unmodifiableMap , bem como em seu JavaDoc .

Inicialmente, o termo " imutável " refere-se a coleções retornadas pelos métodos de fábrica das coleções Java 9 . As coleções em si não puderam ser alteradas de forma alguma (sim, há reflexão , mas não conta); portanto, parece que justificam seu nome. Infelizmente, muitas vezes surge confusão por causa disso. Suponha que exista um método que exiba todos os elementos de uma coleção imutável na tela - sempre dará o mesmo resultado? Hein? Ou não?

Se você não respondeu imediatamente não , significa que você acabou de ver exatamente qual confusão é possível aqui. Uma " coleção imutável de agentes secretos " - parece muito semelhante a uma " coleção imutável de agentes secretos imutáveis " , mas essas duas entidades podem não ser idênticas. Uma coleção imutável não pode ser editada usando operações de inserção / exclusão / limpeza, etc., mas se os agentes secretos são mutáveis ​​(embora os traços de caractere nos filmes de espionagem sejam tão ruins que eles realmente não acreditam), isso não significa que isso que toda a coleção de agentes secretos é imutável. Portanto, agora há uma mudança no sentido de nomear essas coleções como não modificáveis , e não imutáveis , o que é consagrado na nova edição do JavaDoc .

As coleções imutáveis ​​discutidas neste artigo podem conter elementos mutáveis.

Pessoalmente, não gosto de uma revisão dessa terminologia. Na minha opinião, o termo "coleção imutável" deve significar apenas que a coleção em si não pode sofrer alterações, mas não deve caracterizar os elementos nela contidos. Nesse caso, há mais um ponto positivo: o termo “imutabilidade” no ecossistema Java não se transforma em completa bobagem.

De uma forma ou de outra, neste artigo falaremos sobre coleções imutáveis , onde ...

  • As instâncias contidas na coleção são determinadas no estágio de design
  • Essas cópias - uma quantidade uniforme, nem reduzem nem adicionam
  • Nenhuma declaração é feita sobre a mutabilidade desses elementos.

Vamos insistir no fato de que agora praticaremos a adição de coleções imutáveis. Para ser preciso - uma lista imutável. Tudo o que será dito sobre listas é igualmente aplicável a coleções de outros tipos.

Começando a adicionar coleções imutáveis!

Vamos criar a interface ImmutableList e torná-la, em relação à List , uh ... o quê? Supertipo ou subtipo? Vamos nos debruçar sobre a primeira opção.



Lindamente, o ImmutableList não ImmutableList métodos ImmutableList , portanto, usá-lo é sempre seguro, certo? Então ?! Não senhor.

 List<Agent> agents = new ArrayList<>(); // ,  `List`  `ImmutableList` ImmutableList<Agent> section4 = agents; //    section4.forEach(System.out::println); //    `section4` agents.add(new Agent("Motoko"); //  "Motoko" – ,      ?! section4.forEach(System.out::println); 

Este exemplo mostra que é possível transferir uma lista não totalmente imutável para a API, cuja operação pode ser baseada na imutabilidade e, portanto, anular todas as garantias que um nome desse tipo pode sugerir. Aqui está uma receita para você que pode levar ao desastre.

OK, ImmutableList estende a List . Talvez?



Agora, se a API espera uma lista imutável, ela receberá essa lista, mas há duas desvantagens:

  • As listas imutáveis ​​ainda devem oferecer métodos de modificação (conforme definidos no supertipo), e a única implementação possível gerará uma exceção
  • ImmutableList instâncias ImmutableList também ImmutableList instâncias de List e, quando atribuídas a uma variável, transmitidas como argumento ou retornadas desse tipo, é lógico supor que a mutabilidade seja permitida.

Assim, acontece que você só pode usar o ImmutableList localmente, porque ele passa as bordas da API como uma List , o que requer um nível de precaução sobre-humano ou explode em tempo de execução. Não é tão ruim quanto uma List estendendo uma List ImmutableList , mas essa solução ainda está longe do ideal.

Isso é exatamente o que eu tinha em mente quando disse que o tipo ImmutableList da Guava é praticamente inútil. Esse é um ótimo exemplo de código, muito confiável ao trabalhar com listas imutáveis ​​locais (é por isso que eu o uso ativamente), mas, recorrendo a ele, é muito fácil ir além da cidadela compilada garantida e inexpugnável, cujas paredes eram compostas por tipos imutáveis ​​- e somente nessa tipos imutáveis ​​podem revelar completamente seu potencial. Isso é melhor que nada, mas ineficiente como uma solução JDK.

Se o ImmutableList não puder estender a List e a solução alternativa ainda não funcionar, como é que isso fará com que tudo funcione?

Imutabilidade é um recurso


O problema que encontramos nas duas primeiras tentativas de adicionar tipos imutáveis ​​foi nosso equívoco de que a imutabilidade é simplesmente a ausência de algo: pegamos uma List , removemos o código alterado e obtemos uma lista ImmutableList . Mas, de fato, tudo isso não funciona dessa maneira.

Se simplesmente removermos os métodos de modificação da List , obteremos uma lista somente leitura. Ou, seguindo a terminologia formulada acima, ela pode ser chamada UnmodifiableList - ainda pode mudar, mas você não a mudará.

Agora podemos adicionar mais duas coisas a esta imagem:

  • Podemos torná-lo mutável adicionando métodos apropriados
  • Podemos torná-lo imutável adicionando garantias apropriadas.

Imutabilidade não é a ausência de mutabilidade, mas uma garantia de que não haverá mudanças
Nesse caso, é importante entender que, nos dois casos, estamos falando de recursos completos - imutabilidade não é a ausência de alterações, mas uma garantia de que não haverá alterações. Um recurso existente pode não ser necessariamente algo usado para o bem, mas também pode garantir que algo ruim não aconteça no código - nesse caso, pense, por exemplo, na segurança do encadeamento.

Obviamente, mutabilidade e imutabilidade conflitam entre si e, portanto, não podemos usar simultaneamente as duas hierarquias de herança acima mencionadas. Os tipos herdam recursos de outros tipos; portanto, não importa como você os recorte, se um deles herdar do outro, ele conterá os dois recursos.

Tão bem, List e ImmutableList não podem se estender. Mas fomos trazidos para cá trabalhando com UnmodifiableList e, na verdade, os dois tipos têm a mesma API, somente leitura, o que significa que eles devem estendê-la.



Embora eu não chamaria as coisas exatamente desses nomes, a hierarquia desse tipo é razoável. Em Scala, por exemplo, isso é praticamente feito . A diferença é que o supertipo compartilhado, que chamamos de UnmodifiableList , define métodos mutáveis ​​que retornam uma coleção modificada e deixam o original intocado. Assim, uma lista imutável acaba sendo persistente e fornece a uma variante mutável dois conjuntos de métodos de modificação - herdados para receber cópias modificadas e próprias para alterações no local.

E o Java? É possível modernizar essa hierarquia adicionando novos supertipos e irmãos a ela?

É possível melhorar coleções não modificáveis ​​e imutáveis?
Obviamente, não há problema em adicionar os ImmutableList e ImmutableList e criar a hierarquia de herança descrita acima. O problema é que, a curto e médio prazo, será praticamente inútil. Deixe-me explicar.

O ImmutableList ter UnmodifiableList , ImmutableList e List como tipos - nesse caso, as APIs poderão expressar claramente o que precisam e o que oferecem.

 public void payAgents(UnmodifiableList<Agent> agents) { //      , //         } public void sendOnMission(ImmutableList<Agent> agents) { //   ( ), //  ,     } public void downtime(List<Agent> agents) { //       , //        ,      } public UnmodifiableList<Agent> teamRoster() { //   ,     , //      ,     -  } public ImmutableList<Agent> teamOnMission() { //    ,      } public List<Agent> team() { //    ,    , //        } 

No entanto, a menos que você inicie o projeto do zero, provavelmente terá essa funcionalidade e será algo parecido com isto:

 //   ,  `Iterable<Agent>` //  ,   ,        public void payAgents(List<Agent> agents) { } public void sendOnMission(List<Agent> agents) { } public void downtime(List<Agent> agents) { } //      , //    ,  `List`     public List<Agent> teamRoster() { } // ,     `Stream<Agent>` public List<Agent> teamOnMission() { } public List<Agent> team() { } 

Isso não é bom, porque, para que as novas coleções que acabamos de apresentar sejam úteis, precisamos trabalhar com elas! (ufa) O acima é remanescente do código do aplicativo, portanto, a refatoração ImmutableList UnmodifiableList e ImmutableList , e você pode implementá-lo, como foi mostrado na lista acima. Isso pode ser uma grande parte do trabalho, juntamente com confusão quando você precisa organizar a interação do código antigo e atualizado, mas pelo menos parece viável.

E as estruturas, bibliotecas e o próprio JDK? Tudo aqui parece sombrio. Uma tentativa de alterar o parâmetro ou o tipo de retorno de List para ImmutableList levará à incompatibilidade com o código-fonte , ou seja, o código fonte existente não será compilado com a nova versão, pois esses tipos não estão relacionados entre si. Da mesma forma, alterar o tipo de retorno de List para um novo supertipo UnmodifiableList resultará em erros de compilação.

Com a introdução de novos tipos, será necessário fazer alterações e recompilar todo o ecossistema.

No entanto, mesmo se expandirmos o tipo de parâmetro de List para UnmodifiableList , encontraremos um problema, pois essa alteração causa incompatibilidade no nível do bytecode . Quando o código-fonte chama um método, o compilador converte essa chamada em bytecode que faz referência ao método de destino por:

  • O nome da classe cuja instância o destino é declarado
  • Nome do método
  • Tipos de parâmetros de método
  • Tipo de retorno de método

Qualquer alteração no parâmetro ou no tipo de retorno do método fará com que o bytecode indique a assinatura incorreta ao se referir ao método; como resultado, um erro NoSuchMethodError ocorrerá durante a execução. Se a alteração feita for compatível com o código-fonte - por exemplo, se você estiver falando sobre restringir o tipo de retorno ou expandir o tipo do parâmetro - a recompilação deve ser suficiente. No entanto, com mudanças de longo alcance, por exemplo, com a introdução de novas coleções, tudo não é tão simples: para consolidar essas alterações, é necessário recompilar todo o ecossistema Java. Este é um negócio perdido.

A única maneira concebível de tirar proveito dessas novas coleções sem quebrar a compatibilidade é duplicar cada método existente com um novo nome, alterar a API e marcar a versão antiga como indesejável. Você pode imaginar como essa tarefa seria monumental e virtualmente infinita ?!

Reflexão


É claro que tipos de coleções imutáveis ​​são ótimas coisas que eu adoraria ter, mas é improvável que vejamos algo assim no JDK. As implementações competentes de List e ImmutableList nunca ImmutableList se estender (na verdade, elas estendem o mesmo tipo de lista UnmodifiableList , somente leitura), o que dificulta a implementação desses tipos nas APIs existentes.

Fora do contexto de qualquer relacionamento específico entre tipos, a alteração das assinaturas de métodos existentes sempre está repleta de problemas, pois essas alterações são incompatíveis com o bytecode. Quando elas são introduzidas, é necessária pelo menos a recompilação, e essa é uma mudança devastadora que afetará todo o ecossistema Java.

Portanto, acredito que nada disso vai acontecer - nunca e nunca.

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


All Articles