Eficiência do Java "Double Brace Initialization"?

824

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 .

Jim Ferrans
fonte
33
O cheiro do código que vejo aqui é que o leitor ingênuo esperaria flavorsser um HashSet, mas, infelizmente, é uma subclasse anônima.
Elazar Leibovich
6
Se você considerar executar em vez de carregar o desempenho, não há diferença, veja minha resposta.
precisa saber é o seguinte
4
Eu amo que você criou um resumo, acho que este é um exercício que vale a pena para você aumentar a compreensão e a comunidade.
Patrick Murphy
3
Não é obscuro na minha opinião. Os leitores devem saber que uma espera dupla, o @ElazarLeibovich já disse isso em seu comentário . O inicializador de chave dupla em si não existe como uma construção de idioma, é apenas uma combinação de uma subclasse anônima e um inicializador de instância. A única coisa é que as pessoas precisam estar cientes disso.
MC Emperor
8
Java 9 ofertas Definir métodos de fábrica estáticos imutáveis que pode substituir o uso de DCI em algumas situações:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque

Respostas:

607

Aqui está o problema quando eu me empolgo demais com classes internas anônimas:

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

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 classarquivo 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 classarquivos.

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:

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

Parece um recurso "oculto" do Java, mas é apenas uma reescrita de:

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

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:

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

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 ArrayLists com os elementos "Hello"e os "World!"adicione pelo addmétodo, usando os dois métodos:

Método 1: inicialização com cinta dupla

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

Método 2: Instanciar um ArrayListeadd

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

Criei um programa simples para gravar um arquivo de origem Java para executar 1000 inicializações usando os dois métodos:

Teste 1:

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

Teste 2:

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

Observe que o tempo decorrido para inicializar as ArrayListclasses internas 1000 se 1000 anônimas estendidas ArrayListé verificado usando o System.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:

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

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 ArrayListtempo 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 .classgerados 1000 arquivos, compilando o Test1programa de teste de inicialização entre chaves.

coobird
fonte
10
"Provavelmente" é a palavra operativa. A menos que seja medido, nenhuma declaração sobre desempenho é significativa.
Instance Hunter
16
Você fez um trabalho tão bom que eu mal quero dizer isso, mas os tempos do Test1 podem ser dominados por cargas de classe. Seria interessante ver alguém executar uma única instância de cada teste em um loop for, digamos 1.000 vezes, depois executá-lo novamente em um segundo para loop de 1.000 ou 10.000 vezes e imprimir a diferença de horário (System.nanoTime ()). O primeiro loop for deve passar por todos os efeitos de aquecimento (JIT, carga de classe, por exemplo). Ambos os testes modelam diferentes casos de uso. Vou tentar executar isso amanhã no trabalho.
1929 Jim Ferrans
8
@ Jim Ferrans: Estou bastante certo de que os tempos Test1 são de cargas de classe. Mas, a conseqüência do uso da inicialização entre chaves é ter que lidar com as cargas de classe. Acredito que a maioria dos casos de uso de chave dupla init. é para inicialização única, o teste está mais próximo em condições de um caso de uso típico desse tipo de inicialização. Eu acreditaria que várias iterações de cada teste reduziriam o intervalo de tempo de execução.
28409 coobird
73
O que isso prova é que: a) a inicialização com chaves duplas é mais lenta eb) mesmo que você faça isso 1000 vezes, provavelmente não notará a diferença. E também não é esse o gargalo de um loop interno. Ele impõe uma pequena penalidade de uma vez no pior.
Michael Myers
15
Se o uso de DBI tornar o código mais legível ou expressivo, use-o. O fato de aumentar um pouco o trabalho que a JVM deve executar não é, por si só, um argumento válido. Se fosse, então devemos também estar preocupado com auxiliares métodos / aulas extras, preferindo aulas em vez enormes com menos métodos ...
Rogério
105

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).

Segunda pergunta: O novo HashSet deve ser o "this" usado no inicializador de instância ... alguém pode esclarecer o mecanismo? Ingenuamente, esperava que "this" se referisse ao objeto que inicializa "sabores".

É 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.

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

Para esclarecer a subclasse anônima que está sendo criada, você também pode definir métodos. Por exemplo, substituirHashSet.add()

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }
Thilo
fonte
5
Ponto muito bom na referência oculta à classe que contém. No exemplo original, o inicializador da instância está chamando o método add () do novo HashSet <>, não Test.this.add (). Isso sugere para mim que algo mais está acontecendo. Existe uma classe interna anônima para o HashSet <, como sugere Nathan Kitchen?
1929 Jim Ferrans
A referência à classe que contém também pode ser perigosa se estiver envolvida a serialização da estrutura de dados. A classe que contém também será serializada e, portanto, deve ser serializável. Isso pode levar a erros obscuros.
Apenas outro programador Java
56

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:

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

... produzirá estas classes:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

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:

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

O retornado Mapagora conterá uma referência para a instância anexa de ReallyHeavyObject. Você provavelmente não quer arriscar que:

Vazamento de memória aqui

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:

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

Algumas pessoas podem achar isso sintaticamente estimulante.

Lukas Eder
fonte
7
Salve os gatinhos! Boa resposta!
Aris2World
36

Tomando a seguinte classe de teste:

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

e depois descompilar o arquivo de classe, vejo:

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

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):

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

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.

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

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:

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

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:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

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 flavorsnão será contra, HashSetmas sim contra MyHashSet. 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 estende HashSet.

Se você tiver dúvidas sobre os detalhes dos inicializadores de instância, consulte os detalhes na documentação do JLS .

Eddie
fonte
Eddie, explicação muito boa. Se os códigos de bytes da JVM forem tão limpos quanto a descompilação, a velocidade de execução será rápida o suficiente, embora eu me preocupasse um pouco com a bagunça extra dos arquivos .class. Ainda estou curioso para saber por que o construtor do inicializador de instância vê "this" como a nova instância HashSet <> e não a instância Test. Esse comportamento é especificado apenas explicitamente na última Java Language Specification para suportar o idioma?
29119 Jim Ferrans
Eu atualizei minha resposta. Eu deixei de fora o padrão da classe Test, o que causou confusão. Coloquei na minha resposta para tornar as coisas mais óbvias. Menciono também a seção JLS para os blocos inicializadores de instância usados ​​neste idioma.
Eddie
1
@ Jim A interpretação de "isto" não é um caso especial; simplesmente se refere à instância da classe mais interna, que é a subclasse anônima do HashSet <>.
2957 Nathan Kitchen
Desculpe pular quatro anos e meio depois. Mas o interessante do arquivo de classe descompilado (seu segundo bloco de código) é que ele não é Java válido! Tem super()como a segunda linha do construtor implícito, mas precisa vir em primeiro lugar. (Eu testei e ele não será compilado).
chiastic-security
1
@ chiastic-security: Às vezes, os decompiladores geram código que não será compilado.
Edd
35

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.

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}

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, é como new LinkedHashSet(Arrays.asList("xxx","YYY"));

bestsss
fonte
3
Felizmente, a partir do Java 8, o PermGen não é mais uma coisa. Ainda há um impacto, eu acho, mas não um com uma mensagem de erro potencialmente muito obscura.
Joey
2
@ Joey, não faz diferença se a memória for gerenciada diretamente pelo GC (perm gen) ou não. Um vazamento em metaspace ainda é um vazamento, a menos que meta é limitado, não haverá um OOM (de perm gen) por coisas como oom_killer em linux vai chute no.
bestsss
19

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.

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

impressões

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
Peter Lawrey
fonte
2
Não há diferença, exceto que o seu espaço PermGen evaporará se o DBI for usado excessivamente. Pelo menos, será, a menos que você defina algumas opções obscuras da JVM para permitir o descarregamento de classe e a coleta de lixo do espaço PermGen. Dada a prevalência do Java como uma linguagem do lado do servidor, o problema de memória / PermGen merece pelo menos uma menção.
Aroth
1
@aroth este é um bom ponto. Admito que em 16 anos trabalhando em Java, nunca trabalhei em um sistema em que você tinha que ajustar o PermGen (ou Metaspace). Para os sistemas em que trabalhei, o tamanho do código sempre foi mantido razoavelmente pequeno.
Peter Lawrey
2
As condições não deveriam compareCollectionsser 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.
Holger
@aroth apenas como uma atualização: desde o Java 8, a VM não usa mais perm-gen.
Anjo O'Sphere
16

Para criar conjuntos, você pode usar um método de fábrica varargs em vez de inicialização com chaves duplas:

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

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.

Nat
fonte
Hah! ;-) Na verdade, sou um Rip van Winkle retornando ao Java a partir dos 1,2 dias (escrevi o navegador de voz VoiceXML em evolution.voxeo.com em Java). Foi divertido aprender genéricos, tipos parametrizados, Coleções, java.util.concurrent, a nova sintaxe for loop, etc. É uma linguagem melhor agora. A seu ponto, mesmo que o mecanismo por trás do DBI possa parecer obscuro no início, o significado do código deve ser bem claro.
1929 Jim Ferrans
10

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:

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");

A limitação dessa abordagem é obviamente que você não pode controlar o tipo específico de lista a ser gerada.

Paul Morie
fonte
1
Arrays.asList () é o que eu usaria normalmente, mas você está certo, essa situação surge principalmente em testes de unidade; código real construiria as listas de consultas de banco de dados, XML e assim por diante.
1929 Jim Ferrans
7
Cuidado com asList, no entanto: a lista retornada não suporta adicionar ou remover elementos. Sempre que uso como asList, passo a lista resultante para um construtor, como new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))para contornar esse problema.
Michael Myers
7

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.

Neil Coffey
fonte
5

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:

Set<String> flavors = ImmutableSet.of(
    "vanilla", "strawberry", "chocolate", "butter pecan");

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 .

dimo414
fonte
-1. Apesar de alguns pontos válidos, esta resposta se resume a "Como evitar a geração de classe anônima desnecessária? Use uma estrutura com ainda mais classes!"
Agent_L 8/11
1
Eu diria que tudo se resume a "usar a ferramenta certa para o trabalho, em vez de um hack que pode travar seu aplicativo". O goiaba é uma biblioteca bastante comum para aplicativos a serem incluídos (você definitivamente está perdendo se não o estiver usando), mas mesmo que não queira usá-lo, você pode e ainda deve evitar a inicialização entre chaves.
precisa saber é o seguinte
E como exatamente uma inicialização com cinta dupla causaria um vazamento?
Anjo O'Sphere
O @ AngelO'Sphere DBI é uma maneira ofuscada de criar uma classe interna e, portanto, mantém uma referência implícita à sua classe envolvente (a menos que seja usada apenas em staticcontextos). O link com tendência a erros na parte inferior da minha pergunta discute isso ainda mais.
dimo414
Eu diria que é uma questão de gosto. E não há nada realmente ofuscante sobre isso.
Angel O'Sphere
4

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

Fiquei surpreso ao descobrir que, na maioria dos testes de execução, a iniciação interna era realmente mais rápida (quase o dobro em alguns casos). Ao trabalhar com grandes números, o benefício parece desaparecer.

Curiosamente, o caso que cria três objetos no loop perde seus benefícios, mais cedo do que nos outros casos. Não sei ao certo por que isso está acontecendo e mais testes devem ser feitos para chegar a conclusões. Criar implementações concretas pode ajudar a evitar que a definição de classe seja recarregada (se é isso que está acontecendo)

No entanto, é claro que não há muita sobrecarga observada na maioria dos casos para a construção de um único item, mesmo com grandes números.

Um retrocesso seria o fato de que cada uma das iniciações de chaves duplas cria um novo arquivo de classe que adiciona um bloco de disco inteiro ao tamanho do nosso aplicativo (ou cerca de 1k quando compactado). Uma pegada pequena, mas se for usada em muitos lugares, poderá causar um impacto. Use isso 1000 vezes e, potencialmente, você está adicionando um MiB inteiro ao seu aplicativo, o que pode ser preocupante em um ambiente incorporado.

Minha conclusão? Pode ser bom usar, desde que não seja abusado.

Diz-me o que pensas :)

pablisco
fonte
2
Esse não é um teste válido. O código cria objetos sem usá-los, o que permite ao otimizador excluir toda a criação da instância. O único efeito colateral restante é o avanço da sequência de números aleatórios cuja sobrecarga supera qualquer outra coisa nesses testes.
Holger
3

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):

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }
Lawrence Dol
fonte
1
Por quê? O novo objeto será criado no espaço eden e, portanto, requer apenas duas ou três adições de ponteiro para instanciar. A JVM pode perceber que nunca escapa além do escopo do método e, portanto, aloca-a na pilha.
Nat
Sim, é provável que seja mais eficiente que esse código (embora você possa aprimorá-lo informando a HashSetcapacidade sugerida - lembre-se do fator de carga).
Tom Hawtin - tackline
Bem, o construtor HashSet precisa fazer a iteração de qualquer maneira, para que não seja menos eficiente. O código da biblioteca criado para reutilização deve sempre se esforçar para ser o melhor possível.
Lawrence Dol
3

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).

Eric Woodruff
fonte
3

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:

package literal;

public class collection {
    public static <T> List<T> List(T...elems){
        return Arrays.asList( elems );
    }
}

e usa assim:

import static literal.collection.List;
import static system.io.*;

public class CollectionDemo {
    public void demoList(){
        List<String> slist = List( "a", "b", "c" );
        List<Integer> iList = List( 1, 2, 3 );
        for( String elem : List( "a", "java", "list" ) )
            System.out.println( elem );
    }
}

O Google Collections, agora parte do Guava, suporta uma ideia semelhante para a construção de listas. Em entrevista , Jared Levy diz:

[...] os recursos mais usados, que aparecem em quase todas as classes Java que escrevo, são métodos estáticos que reduzem o número de pressionamentos repetidos de teclas no seu código Java. É muito conveniente poder digitar comandos como o seguinte:

Map<OneClassWithALongName, AnotherClassWithALongName> = Maps.newHashMap();

List<String> animals = Lists.immutableList("cat", "dog", "horse");

10/07/2014: Se ao menos pudesse ser tão simples quanto o do Python:

animals = ['cat', 'dog', 'horse']

21/2/2020: No Java 11, agora você pode dizer:

animals = List.of(“cat”, “dog”, “horse”)

Jim Ferrans
fonte
2
  1. 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.

  2. Parece-me que o contexto é o objeto retornado por new, que é o HashSet.

  3. 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.

MC Emperor
fonte