Em Recursos ocultos de Java, a resposta principal menciona a inicialização com cinta dupla , com uma sintaxe muito atraente:
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}};
Esse idioma cria uma classe interna anônima com apenas um inicializador de instância, que "pode usar [...] qualquer método no escopo que o contém".
Pergunta principal: Isso é tão ineficiente quanto parece? Seu uso deve ser limitado a inicializações pontuais? (E, claro, mostrando!)
Segunda pergunta: O novo HashSet deve ser o "this" usado no inicializador de instância ... alguém pode esclarecer o mecanismo?
Terceira pergunta: esse idioma é obscuro demais para ser usado no código de produção?
Resumo: Respostas muito, muito agradáveis, obrigado a todos. Na questão (3), as pessoas acharam que a sintaxe deveria ser clara (embora eu recomende um comentário ocasional, especialmente se o seu código for repassado para desenvolvedores que talvez não estejam familiarizados com ele).
Na pergunta (1), o código gerado deve ser executado rapidamente. Os arquivos .class extras causam confusão no arquivo jar e retardam um pouco a inicialização do programa (obrigado ao @coobird por medir isso). @Thilo apontou que a coleta de lixo pode ser afetada, e o custo de memória para as classes extra carregadas pode ser um fator em alguns casos.
A pergunta (2) acabou sendo mais interessante para mim. Se eu entendo as respostas, o que está acontecendo no DBI é que a classe interna anônima estende a classe do objeto que está sendo construído pelo novo operador e, portanto, possui um valor "this" referente à instância que está sendo construída. Muito arrumado.
No geral, o DBI me parece uma curiosidade intelectual. Coobird e outros salientam que você pode obter o mesmo efeito com Arrays.asList, métodos varargs, Google Collections e os literais Java 7 Collection propostos. Linguagens JVM mais recentes, como Scala, JRuby e Groovy, também oferecem notações concisas para a construção da lista e interoperam bem com o Java. Dado que o DBI atrapalha o caminho da classe, diminui um pouco o carregamento da classe e torna o código um pouco mais obscuro, eu provavelmente evitaria isso. No entanto, pretendo mostrar isso a um amigo que acabou de obter seu SCJP e adora boas justas sobre a semântica Java! ;-) Obrigado a todos!
7/2017: Baeldung tem um bom resumo da inicialização entre chaves e o considera um antipadrão .
12/2017: @Basil Bourque observa que no novo Java 9 você pode dizer:
Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");
Esse é certamente o caminho a percorrer. Se você estiver com uma versão anterior, dê uma olhada no ImmutableSet das coleções do Google .
fonte
flavors
ser umHashSet
, mas, infelizmente, é uma subclasse anônima.Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Respostas:
Aqui está o problema quando eu me empolgo demais com classes internas anônimas:
Essas são todas as classes que foram geradas quando eu estava fazendo um aplicativo simples e utilizaram grandes quantidades de classes internas anônimas - cada classe será compilada em um
class
arquivo separado .A "inicialização entre chaves", como já mencionado, é uma classe interna anônima com um bloco de inicialização de instância, o que significa que uma nova classe é criada para cada "inicialização", tudo com o objetivo de criar normalmente um único objeto.
Considerando que a Java Virtual Machine precisará ler todas essas classes ao usá-las, isso pode levar algum tempo no processo de verificação de código de bytes e tal. Sem mencionar o aumento no espaço em disco necessário para armazenar todos esses
class
arquivos.Parece que há um pouco de sobrecarga ao utilizar a inicialização entre chaves, portanto, provavelmente não é uma boa idéia exagerar nisso. Mas, como Eddie observou nos comentários, não é possível ter certeza absoluta do impacto.
Apenas para referência, a inicialização com chaves duplas é a seguinte:
Parece um recurso "oculto" do Java, mas é apenas uma reescrita de:
Portanto, é basicamente um bloco de inicialização de instância que faz parte de uma classe interna anônima .
A proposta de Literais da coleção de Joshua Bloch para o Project Coin seguiu as seguintes linhas:
Infelizmente, ele não entrou no Java 7 nem 8 e foi arquivado indefinidamente.
Experimentar
Aqui está o experimento simples que eu testei - faça 1000
ArrayList
s com os elementos"Hello"
e os"World!"
adicione peloadd
método, usando os dois métodos:Método 1: inicialização com cinta dupla
Método 2: Instanciar um
ArrayList
eadd
Criei um programa simples para gravar um arquivo de origem Java para executar 1000 inicializações usando os dois métodos:
Teste 1:
Teste 2:
Observe que o tempo decorrido para inicializar as
ArrayList
classes internas 1000 se 1000 anônimas estendidasArrayList
é verificado usando oSystem.currentTimeMillis
, para que o cronômetro não tenha uma resolução muito alta. No meu sistema Windows, a resolução é de 15 a 16 milissegundos.Os resultados para 10 execuções dos dois testes foram os seguintes:
Como pode ser visto, a inicialização de chaves duplas tem um tempo de execução notável de cerca de 190 ms.
Enquanto isso, o
ArrayList
tempo de execução da inicialização foi de 0 ms. Obviamente, a resolução do temporizador deve ser levada em consideração, mas é provável que seja inferior a 15 ms.Portanto, parece haver uma diferença notável no tempo de execução dos dois métodos. Parece que realmente há alguma sobrecarga nos dois métodos de inicialização.
E sim, foram
.class
gerados 1000 arquivos, compilando oTest1
programa de teste de inicialização entre chaves.fonte
Uma propriedade dessa abordagem que não foi apontada até agora é que, como você cria classes internas, toda a classe que contém é capturada em seu escopo. Isso significa que, enquanto o seu conjunto estiver ativo, ele manterá um ponteiro para a instância que contém (
this$0
) e evitará que seja coletado como lixo, o que pode ser um problema.Isso e o fato de uma nova classe ser criada em primeiro lugar, mesmo que um HashSet normal funcione muito bem (ou até melhor), faz com que eu não queira usar essa construção (mesmo que eu realmente anseie pelo açúcar sintático).
É assim que as classes internas funcionam. Eles têm seus próprios
this
, mas também têm ponteiros para a instância pai, para que você também possa chamar métodos no objeto que o contém. No caso de um conflito de nomenclatura, a classe interna (no seu caso, HashSet) tem precedência, mas você pode prefixar "this" com um nome de classe para obter também o método externo.Para esclarecer a subclasse anônima que está sendo criada, você também pode definir métodos. Por exemplo, substituir
HashSet.add()
fonte
Toda vez que alguém usa a inicialização entre chaves, um gatinho é morto.
Além da sintaxe ser incomum e não realmente idiomática (o gosto é discutível, é claro), você está desnecessariamente criando dois problemas significativos em seu aplicativo, sobre os quais recentemente escrevi em detalhes aqui .
1. Você está criando muitas classes anônimas
Cada vez que você usa a inicialização entre chaves, uma nova classe é feita. Por exemplo, este exemplo:
... produzirá estas classes:
Isso representa um pouco de sobrecarga para o seu carregador de classe - por nada! É claro que não levará muito tempo de inicialização se você fizer uma vez. Mas se você fizer isso 20.000 vezes em todo o aplicativo corporativo ... toda essa memória de pilha apenas por um pouco de "açúcar de sintaxe"?
2. Você está potencialmente criando um vazamento de memória!
Se você pegar o código acima e retornar esse mapa a partir de um método, os chamadores desse método poderão estar inconscientemente segurando recursos muito pesados que não podem ser coletados com lixo. Considere o seguinte exemplo:
O retornado
Map
agora conterá uma referência para a instância anexa deReallyHeavyObject
. Você provavelmente não quer arriscar que:Imagem de http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/
3. Você pode fingir que Java possui literais de mapa
Para responder à sua pergunta real, as pessoas têm usado essa sintaxe para fingir que Java tem algo como literais de mapa, semelhante aos literais de matriz existentes:
Algumas pessoas podem achar isso sintaticamente estimulante.
fonte
Tomando a seguinte classe de teste:
e depois descompilar o arquivo de classe, vejo:
Isso não me parece terrivelmente ineficiente. Se eu estivesse preocupado com o desempenho para algo assim, eu faria o perfil. E sua pergunta # 2 é respondida pelo código acima: Você está dentro de um construtor implícito (e inicializador de instância) para sua classe interna, "
this
" portanto se refere a essa classe interna.Sim, essa sintaxe é obscura, mas um comentário pode esclarecer o uso obscuro da sintaxe. Para esclarecer a sintaxe, a maioria das pessoas está familiarizada com um bloco inicializador estático (JLS 8.7 Static Initializers):
Você também pode usar uma sintaxe semelhante (sem a palavra "
static
") para uso do construtor (Inicializadores de Instância JLS 8.6), embora eu nunca tenha visto isso usado no código de produção. Isso é muito menos conhecido.Se você não possui um construtor padrão, o bloco de código entre
{
e}
é transformado em construtor pelo compilador. Com isso em mente, desvendar o código de chave dupla:O bloco de código entre os colchetes mais internos é transformado em um construtor pelo compilador. Os aparelhos mais externos delimitam a classe interna anônima. Para fazer isso, a etapa final de tornar tudo não-anônimo:
Para fins de inicialização, eu diria que não há nenhuma sobrecarga (ou tão pequena que possa ser negligenciada). No entanto, todo uso de
flavors
não será contra,HashSet
mas sim contraMyHashSet
. Provavelmente existe uma pequena sobrecarga (e possivelmente insignificante) nisso. Mas, novamente, antes que eu me preocupasse, eu faria o perfil.Novamente, à sua pergunta nº 2, o código acima é o equivalente lógico e explícito da inicialização entre chaves, e torna óbvio onde "
this
" se refere: À classe interna que se estendeHashSet
.Se você tiver dúvidas sobre os detalhes dos inicializadores de instância, consulte os detalhes na documentação do JLS .
fonte
super()
como a segunda linha do construtor implícito, mas precisa vir em primeiro lugar. (Eu testei e ele não será compilado).propenso a vazamentos
Eu decidi participar. O impacto no desempenho inclui: operação do disco + descompactar (para jar), verificação de classe, espaço de permissão (para a JVM da Hotspot da Sun). No entanto, o pior de tudo: é propenso a vazamentos. Você não pode simplesmente voltar.
Portanto, se o conjunto escapar para qualquer outra parte carregada por um carregador de classes diferente e uma referência for mantida lá, toda a árvore de classes + carregador de classes será vazada. Para evitar isso, é necessária uma cópia no HashMap
new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}})
,. Não é mais tão fofo. Eu não uso o idioma, eu mesmo, é comonew LinkedHashSet(Arrays.asList("xxx","YYY"));
fonte
O carregamento de muitas classes pode adicionar alguns milissegundos ao início. Se a inicialização não for tão crítica e você observar a eficiência das classes após a inicialização, não há diferença.
impressões
fonte
compareCollections
ser combinadas com||
e não&&
? O uso&&
parece não apenas semanticamente errado, mas também neutraliza a intenção de medir o desempenho, pois apenas a primeira condição será testada. Além disso, um otimizador inteligente pode reconhecer que as condições nunca mudarão durante as iterações.Para criar conjuntos, você pode usar um método de fábrica varargs em vez de inicialização com chaves duplas:
A biblioteca do Google Collections possui muitos métodos de conveniência como esse, além de várias outras funcionalidades úteis.
Quanto à obscuridade do idioma, eu o encontro e o uso no código de produção o tempo todo. Eu ficaria mais preocupado com os programadores que ficam confusos com o idioma que está autorizado a escrever o código de produção.
fonte
Além da eficiência, raramente me vejo desejando a criação de coleções declarativas fora dos testes de unidade. Eu acredito que a sintaxe de chave dupla é muito legível.
Outra maneira de conseguir especificamente a construção declarativa de listas é usar
Arrays.asList(T ...)
o seguinte:A limitação dessa abordagem é obviamente que você não pode controlar o tipo específico de lista a ser gerada.
fonte
new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))
para contornar esse problema.Geralmente não há nada particularmente ineficiente nisso. Geralmente, não importa para a JVM que você criou uma subclasse e adicionou um construtor a ela - isso é uma coisa normal e cotidiana a ser feita em uma linguagem orientada a objetos. Posso pensar em casos bem planejados em que você poderia causar uma ineficiência fazendo isso (por exemplo, você tem um método chamado repetidamente que acaba tendo uma mistura de classes diferentes por causa dessa subclasse, enquanto que a classe passada seria totalmente previsível - - neste último caso, o compilador JIT pode fazer otimizações que não são viáveis no primeiro). Mas, na verdade, acho que os casos em que isso será importante são muito artificiais.
Eu veria a questão mais do ponto de vista se você deseja "desorganizar as coisas" com muitas classes anônimas. Como um guia geral, considere usar o idioma não mais do que você usaria, digamos, classes anônimas para manipuladores de eventos.
Em (2), você está dentro do construtor de um objeto, então "isto" se refere ao objeto que você está construindo. Isso não é diferente de qualquer outro construtor.
Quanto ao (3), isso realmente depende de quem está mantendo seu código, eu acho. Se você não sabe disso com antecedência, então uma referência que eu sugiro usar é "você vê isso no código-fonte do JDK?" (neste caso, não me lembro de ter visto muitos inicializadores anônimos, e certamente não nos casos em que esse é o único conteúdo da classe anônima). Na maioria dos projetos de tamanho moderado, eu diria que você realmente precisará que seus programadores entendam a fonte do JDK em algum momento ou outro; portanto, qualquer sintaxe ou idioma usado lá é um "jogo justo". Além disso, eu diria, treine as pessoas nessa sintaxe se você tiver controle de quem está mantendo o código, caso contrário, comente ou evite.
fonte
A inicialização entre chaves é um hack desnecessário que pode introduzir vazamentos de memória e outros problemas
Não há razão legítima para usar esse "truque". O Guava fornece boas coleções imutáveis que incluem fábricas e construtores estáticos, permitindo que você preencha sua coleção onde ela é declarada em uma sintaxe limpa, legível e segura .
O exemplo na pergunta se torna:
Isso não é apenas mais curto e fácil de ler, mas evita os numerosos problemas com o padrão de dupla espinha descrito em outras respostas . Certamente, ele funciona de maneira semelhante a um construído diretamente
HashMap
, mas é perigoso e propenso a erros, e há opções melhores.Sempre que você se deparar com considerações sobre inicialização dupla, você deve reexaminar suas APIs ou introduzir novas para resolver adequadamente o problema, em vez de aproveitar os truques sintáticos.
Propenso a erros agora sinaliza esse anti-padrão .
fonte
static
contextos). O link com tendência a erros na parte inferior da minha pergunta discute isso ainda mais.Eu estava pesquisando isso e decidi fazer um teste mais aprofundado do que o fornecido pela resposta válida.
Aqui está o código: https://gist.github.com/4368924
e esta é a minha conclusão
Diz-me o que pensas :)
fonte
Segundo a resposta de Nat, exceto que eu usaria um loop em vez de criar e lançar imediatamente a lista implícita de asList (elementos):
fonte
HashSet
capacidade sugerida - lembre-se do fator de carga).Embora essa sintaxe possa ser conveniente, ela também adiciona muitas dessas referências de US $ 0 à medida que elas ficam aninhadas e pode ser difícil depurar os inicializadores, a menos que sejam definidos pontos de interrupção em cada uma delas. Por esse motivo, eu recomendo usar isso apenas para setters banais, especialmente configurados para constantes, e locais onde subclasses anônimas não importam (como nenhuma serialização envolvida).
fonte
Mario Gleichman descreve como usar funções genéricas do Java 1.5 para simular literais do Scala List, embora, infelizmente, você acabe com listas imutáveis .
Ele define esta classe:
e usa assim:
O Google Collections, agora parte do Guava, suporta uma ideia semelhante para a construção de listas. Em entrevista , Jared Levy diz:
10/07/2014: Se ao menos pudesse ser tão simples quanto o do Python:
21/2/2020: No Java 11, agora você pode dizer:
fonte
Isso chamará
add()
cada membro. Se você puder encontrar uma maneira mais eficiente de colocar itens em um conjunto de hash, use-o. Observe que a classe interna provavelmente gerará lixo, se você for sensível a isso.Parece-me que o contexto é o objeto retornado por
new
, que é oHashSet
.Se precisar perguntar ... Mais provável: as pessoas que vierem depois de você saberem disso ou não? É fácil de entender e explicar? Se você puder responder "sim" a ambos, fique à vontade para usá-lo.
fonte