Efficienza della "doppia inizializzazione" di Java?


824

In Hidden Features of Java la risposta principale menziona Double Brace Initialization , con una sintassi molto allettante:

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

Questo idioma crea una classe interna anonima con solo un inizializzatore di istanza, che "può usare qualsiasi [...] metodo nell'ambito di contenimento".

Domanda principale: è inefficiente come sembra? Il suo utilizzo dovrebbe essere limitato alle inizializzazioni una tantum? (E ovviamente in mostra!)

Seconda domanda: il nuovo HashSet deve essere il "questo" utilizzato nell'inizializzatore dell'istanza ... qualcuno può far luce sul meccanismo?

Terza domanda: questo idioma è troppo oscuro per essere usato nel codice di produzione?

Riepilogo: risposte molto, molto belle, grazie a tutti. Alla domanda (3), la gente pensava che la sintassi dovesse essere chiara (anche se consiglierei un commento occasionale, specialmente se il tuo codice passerà agli sviluppatori che potrebbero non averne familiarità).

Alla domanda (1), il codice generato dovrebbe essere eseguito rapidamente. I file extra .class causano il disordine dei file jar e rallentano leggermente l'avvio del programma (grazie a @coobird per averlo misurato). @Thilo ha sottolineato che la garbage collection può essere influenzata e il costo della memoria per le classi extra caricate può essere un fattore in alcuni casi.

La domanda (2) si è rivelata molto interessante per me. Se capisco le risposte, quello che sta succedendo in DBI è che la classe interna anonima estende la classe dell'oggetto che viene costruito dal nuovo operatore, e quindi ha un valore "this" che fa riferimento all'istanza in costruzione. Molto pulito.

Nel complesso, DBI mi sembra una sorta di curiosità intellettuale. Coobird e altri sottolineano che è possibile ottenere lo stesso effetto con Arrays.asList, i metodi varargs, le raccolte Google e i letterali della collezione Java 7 proposti. Le lingue JVM più recenti come Scala, JRuby e Groovy offrono anche notazioni concise per la costruzione di elenchi e interagiscono bene con Java. Dato che DBI ingombra il percorso di classe, rallenta un po 'il caricamento della classe e rende il codice un po' più oscuro, probabilmente lo eviterei. Tuttavia, ho in programma di lanciarlo su un amico che ha appena ottenuto il suo SCJP e adora i bravi giullari sulla semantica di Java! ;-) Grazie a tutti!

7/2017: Baeldung ha un buon riassunto dell'inizializzazione del doppio controvento e lo considera un anti-pattern.

12/2017: @Basil Bourque osserva che nel nuovo Java 9 puoi dire:

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

Questa è sicuramente la strada da percorrere. Se sei bloccato con una versione precedente, dai un'occhiata a ImmutableSet di Google Collections .


33
L'odore di codice che vedo qui è che l'ingenuo lettore si aspetterebbe flavorsdi essere un HashSet, ma purtroppo è una sottoclasse anonima.
Elazar Leibovich,

6
Se consideri l'esecuzione anziché il caricamento delle prestazioni non c'è alcuna differenza, vedi la mia risposta.
Peter Lawrey,

4
Adoro il fatto che tu abbia creato un riassunto, penso che questo sia un esercizio utile per te sia per aumentare la comprensione che per la comunità.
Patrick Murphy,

3
Non è oscuro secondo me. I lettori dovrebbero sapere che un doppio ... o aspetta, @ElazarLeibovich lo ha già detto nel suo commento . Lo stesso inizializzatore a doppia parentesi non esiste come costrutto di un linguaggio, è solo una combinazione di una sottoclasse anonima e un inizializzatore di istanza. L'unica cosa è che le persone devono esserne consapevoli.
MC Emperor

8
Java 9 offre metodi statici di fabbrica immutabili che possono sostituire l'uso del DCI in alcune situazioni:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque,

Risposte:


607

Ecco il problema quando sono troppo preso dalle classi interne anonime:

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

Queste sono tutte classi che sono state generate quando stavo facendo una semplice applicazione, e hanno usato copiose quantità di classi interne anonime: ogni classe verrà compilata in un classfile separato .

La "inizializzazione a doppia parentesi", come già accennato, è una classe interna anonima con un blocco di inizializzazione dell'istanza, il che significa che viene creata una nuova classe per ogni "inizializzazione", tutto allo scopo di creare normalmente un singolo oggetto.

Considerando che la Java Virtual Machine dovrà leggere tutte quelle classi quando le si utilizza, ciò può richiedere del tempo nel processo di verifica del bytecode e simili. Per non parlare dell'aumento dello spazio su disco necessario per archiviare tutti quei classfile.

Sembra che ci sia un po 'di sovraccarico quando si utilizza l'inizializzazione a doppia parentesi, quindi probabilmente non è una buona idea esagerare. Ma come ha osservato Eddie nei commenti, non è possibile essere assolutamente sicuri dell'impatto.


Solo per riferimento, l'inizializzazione con doppio controvento è la seguente:

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

Sembra una funzionalità "nascosta" di Java, ma è solo una riscrittura di:

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

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

Quindi è fondamentalmente un blocco di inizializzazione dell'istanza che fa parte di una classe interna anonima .


La proposta letterale della Collezione Joshua Bloch per Project Coin era sulla falsariga di:

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

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

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

Purtroppo, non si è fatto strada né in Java 7 né in 8 ed è stato accantonato indefinitamente.


Sperimentare

Ecco il semplice esperimento che ho testato: fai 1000 ArrayLists con gli elementi "Hello"e "World!"li aggiungi tramite il addmetodo, usando i due metodi:

Metodo 1: inizializzazione doppia parentesi graffa

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

Metodo 2: creare un'istanza di un ArrayListeadd

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

Ho creato un semplice programma per scrivere un file sorgente Java per eseguire 1000 inizializzazioni usando i due metodi:

Test 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);
  }
}

Test 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);
  }
}

Si noti che il tempo trascorso per inizializzare l' ArrayListestensione di 1000 se 1000 classi interne anonime ArrayListviene verificato usando il System.currentTimeMillis, quindi il timer non ha una risoluzione molto alta. Sul mio sistema Windows, la risoluzione è di circa 15-16 millisecondi.

I risultati per 10 esecuzioni dei due test sono stati i seguenti:

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

Come si può vedere, l'inizializzazione del doppio controvento ha un notevole tempo di esecuzione di circa 190 ms.

Nel frattempo, il ArrayListtempo di esecuzione dell'inizializzazione è risultato essere 0 ms. Naturalmente, la risoluzione del timer dovrebbe essere presa in considerazione, ma è probabile che sia inferiore a 15 ms.

Quindi, sembra esserci una notevole differenza nel tempo di esecuzione dei due metodi. Sembra che ci sia davvero un certo sovraccarico nei due metodi di inizializzazione.

E sì, sono stati .classgenerati 1000 file compilando il Test1programma di test di inizializzazione del doppio controvento.


10
"Probabilmente" è la parola chiave. Se non misurato, nessuna affermazione sulle prestazioni è significativa.
Istanza Hunter,

16
Hai fatto un ottimo lavoro, non voglio dirlo, ma i tempi di Test1 potrebbero essere dominati da carichi di classe. Sarebbe interessante vedere qualcuno eseguire una singola istanza di ogni test in un ciclo for dire 1.000 volte, quindi eseguirlo di nuovo in un secondo per ciclo di 1.000 o 10.000 volte e stampare la differenza di tempo (System.nanoTime ()). Il primo ciclo dovrebbe superare tutti gli effetti di riscaldamento (JIT, classload, ad es.). Entrambi i test modellano tuttavia diversi casi d'uso. Proverò a eseguirlo domani al lavoro.
Jim Ferrans,

8
@Jim Ferrans: sono abbastanza certo che i tempi di Test1 provengano da carichi di classe. Tuttavia, la conseguenza dell'utilizzo dell'inizializzazione a doppia parentesi è la necessità di far fronte ai carichi di classe. Credo che la maggior parte dei casi d'uso per init a doppio controvento. è per l'inizializzazione una tantum, il test è più vicino in condizioni a un tipico caso d'uso di questo tipo di inizializzazione. Ritengo che più iterazioni di ciascun test ridurrebbero l'intervallo di tempo di esecuzione.
coobird

73
Ciò dimostra che a) l'inizializzazione a doppia parentesi è più lenta e b) anche se lo fai 1000 volte, probabilmente non noterai la differenza. E non è che questo potrebbe essere il collo di bottiglia in un ciclo interno. Impone una piccola penalità una tantum AL MOLTO PEGGIORE.
Michael Myers

15
Se l'utilizzo di DBI rende il codice più leggibile o espressivo, quindi utilizzarlo. Il fatto che aumenti un po 'il lavoro che la JVM deve svolgere non è un argomento valido, di per sé, contro di esso. Se lo fosse, dovremmo anche essere preoccupati per i metodi / le classi di supporto extra, preferendo invece le classi enormi con meno metodi ...
Rogério,

105

Una proprietà di questo approccio che non è stata finora sottolineata è che, poiché si creano classi interne, l'intera classe contenente viene catturata nel suo ambito. Ciò significa che finché il tuo Set è attivo, manterrà un puntatore all'istanza contenente ( this$0) e impedirà che venga raccolto in modo inutile, il che potrebbe essere un problema.

Questo e il fatto che una nuova classe venga creata in primo luogo anche se un normale HashSet funzionerebbe bene (o anche meglio), non mi fa venir voglia di usare questo costrutto (anche se desidero davvero lo zucchero sintattico).

Seconda domanda: il nuovo HashSet deve essere il "questo" utilizzato nell'inizializzatore dell'istanza ... qualcuno può far luce sul meccanismo? Mi sarei ingenuamente aspettato che "questo" si riferisse all'oggetto che inizializzava "sapori".

Questo è il modo in cui funzionano le classi interne. Prendono il loro this, ma hanno anche dei puntatori all'istanza padre, in modo che tu possa anche chiamare metodi sull'oggetto contenitore. In caso di conflitto di denominazione, la classe interna (nel tuo caso HashSet) ha la precedenza, ma puoi aggiungere il prefisso "this" con un nome di classe per ottenere anche il metodo esterno.

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 
            }
        };
    }
}

Per essere chiari sulla sottoclasse anonima in fase di creazione, è possibile definire anche metodi all'interno. Ad esempio sovrascrivereHashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }

5
Ottimo punto sul riferimento nascosto alla classe contenente. Nell'esempio originale, l'inizializzatore dell'istanza chiama il metodo add () del nuovo HashSet <String>, non Test.this.add (). Questo mi suggerisce che sta succedendo qualcos'altro. Esiste una classe interna anonima per l'HashSet <String>, come suggerisce Nathan Kitchen?
Jim Ferrans,

Il riferimento alla classe contenente potrebbe anche essere pericoloso se è coinvolta la serializzazione della struttura dati. Anche la classe contenente verrà serializzata e quindi deve essere serializzabile. Questo può portare a errori oscuri.
Solo un altro programmatore Java il

56

Ogni volta che qualcuno usa l'inizializzazione con doppio tutore, un gattino viene ucciso.

A parte la sintassi che è piuttosto insolita e non proprio idiomatica (il gusto è discutibile, ovviamente), stai creando inutilmente due problemi significativi nella tua applicazione, di cui ho recentemente scritto un blog più dettagliatamente qui .

1. Stai creando troppe lezioni anonime

Ogni volta che si utilizza l'inizializzazione con doppio controvento, viene creata una nuova classe. Ad esempio questo esempio:

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");
        }});
    }});
}};

... produrrà queste classi:

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

È un bel po 'di sovraccarico per il tuo classloader - per niente! Ovviamente non ci vorrà molto tempo di inizializzazione se lo fai una volta. Ma se lo fai 20'000 volte in tutta la tua applicazione aziendale ... tutta quella memoria dell'heap solo per un po 'di "zucchero della sintassi"?

2. Stai potenzialmente creando una perdita di memoria!

Se si prende il codice sopra riportato e si restituisce quella mappa da un metodo, i chiamanti di quel metodo potrebbero trattenere inconsapevolmente risorse molto pesanti che non possono essere raccolte. Considera il seguente esempio:

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;
    }
}

Il reso Mapora conterrà un riferimento all'istanza allegata di ReallyHeavyObject. Probabilmente non vuoi rischiare che:

Perdita di memoria proprio qui

Immagine da http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3. Puoi fingere che Java abbia i letterali delle mappe

Per rispondere alla tua vera domanda, le persone hanno utilizzato questa sintassi per fingere che Java abbia qualcosa di simile ai letterali delle mappe, simili ai letterali dell'array esistenti:

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

Alcune persone possono trovare questo sintatticamente stimolante.


7
Salva i cuccioli! Buona risposta!
Aris2World,

36

Prendere la seguente classe di test:

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

e poi decompilando il file di classe, vedo:

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");
      }
    };
  }
}

Questo non mi sembra terribilmente inefficiente. Se fossi preoccupato per le prestazioni per qualcosa del genere, la profilerei. E la tua domanda n. 2 risponde al codice sopra: sei all'interno di un costruttore implicito (e inizializzatore di istanza) per la tua classe interna, quindi " this" si riferisce a questa classe interna.

Sì, questa sintassi è oscura, ma un commento può chiarire l'uso oscuro della sintassi. Per chiarire la sintassi, la maggior parte delle persone ha familiarità con un blocco di inizializzatore statico (inizializzatori statici JLS 8.7):

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

Puoi anche usare una sintassi simile (senza la parola " static") per l'uso del costruttore (JLS 8.6 Initializer Initializer), anche se non l'ho mai visto usato nel codice di produzione. Questo è molto meno comunemente noto.

public class Sample2 {
    private final String someVar;

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

Se non si dispone di un costruttore predefinito, il blocco di codice tra {e }viene trasformato in un costruttore dal compilatore. Con questo in mente, svela il codice del doppio controvento:

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

Il blocco di codice tra le parentesi più interne viene trasformato in un costruttore dal compilatore. Le parentesi più esterne delimitano la classe interna anonima. Per fare questo l'ultimo passo per rendere tutto non anonimo:

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

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

Ai fini dell'inizializzazione, direi che non vi è alcun sovraccarico (o così piccolo da poter essere trascurato). Tuttavia, ogni uso di flavorsnon andrà contro HashSetma invece contro MyHashSet. C'è probabilmente un piccolo (e probabilmente trascurabile) sovraccarico in questo. Ma ancora una volta, prima che mi preoccupassi, lo profilerei.

Ancora una volta, alla tua domanda n. 2, il codice sopra è l'equivalente logico ed esplicito dell'inizializzazione della doppia parentesi graffa e rende ovvio dove " this" si riferisce: alla classe interna che si estende HashSet.

In caso di domande sui dettagli degli inizializzatori dell'istanza, consultare i dettagli nella documentazione JLS .


Eddie, spiegazione molto bella. Se i codici byte JVM sono puliti come la decompilazione, la velocità di esecuzione sarà abbastanza veloce, anche se sarei un po 'preoccupato per il disordine dei file .class extra. Sono ancora curioso di sapere perché il costruttore dell'inizializzatore dell'istanza vede "this" come la nuova istanza di HashSet <String> e non l'istanza Test. Questo è solo un comportamento esplicitamente specificato nell'ultima specifica del linguaggio Java per supportare il linguaggio?
Jim Ferrans,

Ho aggiornato la mia risposta. Ho lasciato fuori la piastra della caldaia della classe Test, che ha causato confusione. L'ho inserito nella mia risposta per rendere le cose più ovvie. Cito anche la sezione JLS per i blocchi di inizializzazione dell'istanza utilizzati in questo idioma.
Eddie,

1
@Jim L'interpretazione di "questo" non è un caso speciale; si riferisce semplicemente all'istanza della classe racchiusa più interna, che è la sottoclasse anonima di HashSet <String>.
Nathan Kitchen,

Mi spiace saltare tra quattro anni e mezzo dopo. Ma la cosa bella del file di classe decompilato (il tuo secondo blocco di codice) è che non è Java valido! Ha super()come seconda linea il costruttore implicito, ma deve venire prima. (L'ho provato e non verrà compilato.)
chiastic-security

1
@ chiastic-security: a volte i decompilatori generano codice che non viene compilato.
Eddie,

35

soggetto a perdite

Ho deciso di intervenire. L'impatto sulle prestazioni include: funzionamento del disco + decompressione (per jar), verifica della classe, spazio perm-gen (per l'Hotspot JVM di Sun). Tuttavia, il peggio di tutto: è soggetto a perdite. Non puoi semplicemente tornare.

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

Quindi, se il set fuoriesce da qualsiasi altra parte caricata da un diverso classloader e un riferimento viene mantenuto lì, l'intero albero di classi + classloader trapelerà. Per evitare che, una copia di HashMap è necessario, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Non più così carino. Non uso il linguaggio, me stesso, invece è come new LinkedHashSet(Arrays.asList("xxx","YYY"));


3
Fortunatamente, a partire da Java 8, PermGen non è più una cosa. C'è ancora un impatto, immagino, ma non uno con un messaggio di errore potenzialmente molto oscuro.
Joey,

2
@Joey, non fa differenza se la memoria è gestita direttamente dal GC (permesso) o no. Una perdita di metaspace è ancora una perdita, a meno che la meta non sia limitata, non ci sarà una OOM (fuori dal perm gen) da parte di cose come oom_killer in Linux sta per entrare.
bestsss

19

Il caricamento di molte classi può aggiungere alcuni millisecondi all'inizio. Se l'avvio non è così critico e si osserva l'efficienza delle classi dopo l'avvio, non c'è differenza.

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();
    }
}

stampe

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

2
Nessuna differenza se non che lo spazio PermGen evapora se DBI viene utilizzato eccessivamente. Almeno, a meno che tu non imposti alcune oscure opzioni JVM per consentire lo scarico di classe e la garbage collection dello spazio PermGen. Data la prevalenza di Java come linguaggio lato server, il problema memoria / PermGen merita almeno una menzione.
aroth

1
@aroth questo è un buon punto. Devo ammettere che in 16 anni di lavoro su Java non ho mai lavorato su un sistema in cui è stato necessario ottimizzare PermGen (o Metaspace). Per i sistemi su cui ho lavorato la dimensione del codice è stata sempre mantenuta ragionevolmente piccola.
Peter Lawrey,

2
Le condizioni non dovrebbero compareCollectionsessere combinate ||piuttosto che &&? L'uso &&non sembra solo semanticamente sbagliato, ma contrasta l'intenzione di misurare le prestazioni, poiché verrà testata solo la prima condizione. Inoltre, un ottimizzatore intelligente può riconoscere che le condizioni non cambieranno mai durante le iterazioni.
Holger,

@aroth solo come aggiornamento: da Java 8 la VM non utilizza più perm-gen.
Angel O'Sphere,

16

Per creare set è possibile utilizzare un metodo factory varargs invece di inizializzazione a doppio controvento:

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

La biblioteca di Google Collections ha molti metodi di praticità come questo, oltre a molte altre utili funzionalità.

Per quanto riguarda l'oscurità del linguaggio, lo incontro e lo uso sempre nel codice di produzione. Sarei più preoccupato per i programmatori che vengono confusi dal linguaggio che è autorizzato a scrivere codice di produzione.


Hah! ;-) In realtà sono un Rip van Winkle che ritorna a Java da 1,2 giorni (ho scritto il browser web vocale VoiceXML su evolution.voxeo.com in Java). È stato divertente imparare generici, tipi con parametri, Collezioni, java.util.concurrent, il nuovo per la sintassi del loop, ecc. Ora è un linguaggio migliore. A tuo avviso, anche se all'inizio il meccanismo alla base di DBI può sembrare oscuro, il significato del codice dovrebbe essere abbastanza chiaro.
Jim Ferrans,

10

A parte l'efficienza, raramente mi ritrovo a desiderare la creazione di raccolte dichiarative al di fuori dei test unitari. Credo che la sintassi del doppio controvento sia molto leggibile.

Un altro modo per ottenere specificamente la costruzione dichiarativa di elenchi è quello di utilizzare in questo Arrays.asList(T ...)modo:

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

La limitazione di questo approccio è ovviamente che non è possibile controllare il tipo specifico di elenco da generare.


1
Arrays.asList () è quello che userei normalmente, ma hai ragione, questa situazione si presenta principalmente nei test unitari; il codice reale costruirà gli elenchi da query DB, XML e così via.
Jim Ferrans,

7
Attenzione a asList, tuttavia: l'elenco restituito non supporta l'aggiunta o la rimozione di elementi. Ogni volta che utilizzo asList, passo l'elenco risultante in un costruttore come new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))per aggirare questo problema.
Michael Myers

7

In genere non c'è nulla di particolarmente inefficiente al riguardo. In genere per JVM non importa che tu abbia creato una sottoclasse e aggiunto un costruttore: è una cosa normale e quotidiana da fare in un linguaggio orientato agli oggetti. Posso pensare a casi abbastanza inventati in cui potresti causare un'inefficienza facendo questo (ad esempio hai un metodo chiamato ripetutamente che finisce per prendere una miscela di classi diverse a causa di questa sottoclasse, mentre ordinaria la classe passata sarebbe totalmente prevedibile- - in quest'ultimo caso, il compilatore JIT potrebbe effettuare ottimizzazioni non realizzabili nel primo). Ma davvero, penso che i casi in cui avrà importanza siano molto inventati.

Vedrei il problema più dal punto di vista del fatto che tu voglia "ingombrare le cose" con molte classi anonime. Come guida approssimativa, considera di usare il linguaggio non più di quanto useresti, diciamo, classi anonime per i gestori di eventi.

In (2), sei all'interno del costruttore di un oggetto, quindi "this" si riferisce all'oggetto che stai costruendo. Non è diverso da qualsiasi altro costruttore.

Per quanto riguarda (3), ciò dipende davvero da chi mantiene il codice, immagino. Se non lo conosci in anticipo, un benchmark che suggerirei di utilizzare è "Lo vedi nel codice sorgente di JDK?" (in questo caso, non ricordo di aver visto molti inizializzatori anonimi, e certamente non nei casi in cui questo è l' unico contenuto della classe anonima). Nei progetti di dimensioni più moderate, direi che avrai davvero bisogno che i tuoi programmatori comprendano la fonte JDK in un punto o nell'altro, quindi qualsiasi sintassi o linguaggio usato lì è "fair game". Oltre a ciò, direi, formare le persone su quella sintassi se si ha il controllo su chi mantiene il codice, altrimenti commentare o evitare.


5

L'inizializzazione a doppia parentesi è un hack non necessario che può introdurre perdite di memoria e altri problemi

Non c'è motivo legittimo per usare questo "trucco". Guava offre belle e immutabili raccolte che includono sia fabbriche statiche che costruttori, permettendoti di popolare la tua raccolta in cui è dichiarata in una sintassi pulita, leggibile e sicura .

L'esempio nella domanda diventa:

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

Non solo è più breve e più facile da leggere, ma evita i numerosi problemi con il modello a doppio rinforzo descritto in altre risposte . Certo, si comporta in modo simile a quello costruito direttamente HashMap, ma è pericoloso e soggetto a errori e ci sono opzioni migliori.

Ogni volta che ti trovi a prendere in considerazione l'inizializzazione a doppia parentesi, dovresti riesaminare le tue API o introdurne di nuove per affrontare correttamente il problema, anziché trarre vantaggio dai trucchi sintattici.

Error-Prone ora segnala questo anti-pattern .


-1. Nonostante alcuni punti validi, questa risposta si riduce a "Come evitare di generare classi anonime non necessarie? Utilizzare un framework, con ancora più classi!"
Agent_L

1
Direi che si riduce a "utilizzare lo strumento giusto per il lavoro, piuttosto che un hack che può causare il crash dell'applicazione". Guava è una libreria piuttosto comune per le applicazioni da includere (sicuramente ti perdi se non la stai usando), ma anche se non vuoi usarla puoi e dovresti comunque evitare l'inizializzazione a doppia parentesi.
dimo414

E come esattamente un'inizializzazione a doppia parentesi causerebbe una perdita di memoria?
Angel O'Sphere,

@ AngelO'Sphere DBI è un modo offuscato di creare una classe interiore , e quindi mantiene un riferimento implicito alla sua classe chiusa (a meno che non sia mai stato usato solo in staticcontesti). Il link Error-Prone in fondo alla mia domanda ne discute ulteriormente.
dimo414,

Direi che è una questione di gusti. E non c'è nulla di veramente offuscante al riguardo.
Angel O'Sphere,

4

Stavo studiando questo e ho deciso di fare un test più approfondito di quello fornito dalla risposta valida.

Ecco il codice: https://gist.github.com/4368924

e questa è la mia conclusione

Sono stato sorpreso di scoprire che nella maggior parte dei test di esecuzione l'iniziazione interna era in realtà più rapida (quasi doppia in alcuni casi). Quando si lavora con grandi numeri, il vantaggio sembra svanire.

È interessante notare che il caso che crea 3 oggetti sul loop perde i suoi benefici che si manifestano prima degli altri casi. Non sono sicuro del perché questo accada e dovrebbero essere condotti ulteriori test per giungere a conclusioni. La creazione di implementazioni concrete può aiutare a evitare di ricaricare la definizione della classe (se è quello che sta succedendo)

Tuttavia, è chiaro che nella maggior parte dei casi non è stato osservato molto overhead per la costruzione di singoli elementi, anche con un numero elevato.

Uno svantaggio sarebbe il fatto che ciascuna delle iniziazioni a doppio controvento crea un nuovo file di classe che aggiunge un intero blocco del disco alla dimensione della nostra applicazione (o circa 1k quando compresso). Un ingombro ridotto, ma se utilizzato in molti luoghi potrebbe potenzialmente avere un impatto. Usalo 1000 volte e potresti aggiungere potenzialmente un MiB intero alla tua applicazione, il che potrebbe riguardare un ambiente incorporato.

La mia conclusione? Può essere ok da usare fino a quando non viene abusato.

Fatemi sapere cosa ne pensate :)


2
Questo non è un test valido. Il codice crea oggetti senza usarli, il che consente all'ottimizzatore di eludere l'intera creazione dell'istanza. L'unico effetto collaterale rimanente è l'avanzamento della sequenza numerica casuale il cui overhead supera comunque qualsiasi altra cosa in questi test.
Holger,

3

Io secondo la risposta di Nat, tranne che userei un ciclo invece di creare e lanciare immediatamente la Lista implicita da asList (elementi):

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }

1
Perché? Il nuovo oggetto verrà creato nello spazio eden e quindi sono necessarie solo due o tre aggiunte al puntatore per creare un'istanza. La JVM potrebbe notare che non sfugge mai oltre l'ambito del metodo e quindi allocarlo nello stack.
Nat

Sì, è probabile che finisca per essere più efficiente di quel codice (anche se puoi migliorarlo raccontando la HashSetcapacità suggerita - ricorda il fattore di carico).
Tom Hawtin - tackline

Bene, il costruttore di HashSet deve comunque eseguire l'iterazione, quindi non sarà meno efficiente. Il codice della libreria creato per il riutilizzo dovrebbe sempre cercare di essere il migliore possibile.
Lawrence Dol,

3

Mentre questa sintassi può essere conveniente, aggiunge anche molti di questi riferimenti $ 0 man mano che questi vengono annidati e può essere difficile eseguire il debug negli inizializzatori a meno che non siano impostati punti di interruzione su ciascuno di essi. Per questo motivo, consiglio di usarlo solo per setter banali, specialmente se impostati su costanti, e in luoghi in cui le sottoclassi anonime non contano (come nessuna serializzazione coinvolta).


3

Mario Gleichman descrive come usare le funzioni generiche di Java 1.5 per simulare i letterali della Scala List, anche se purtroppo ti ritrovi con liste immutabili .

Definisce questa classe:

package literal;

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

e lo usa così:

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 );
    }
}

Google Collections, ora parte di Guava, supporta un'idea simile per la costruzione di elenchi. In questa intervista , Jared Levy afferma:

[...] le funzionalità più utilizzate, che compaiono in quasi tutte le classi Java che scrivo, sono metodi statici che riducono il numero di sequenze di tasti ripetitivi nel codice Java. È così conveniente poter inserire comandi come i seguenti:

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

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

7/10/2014: Se solo potesse essere semplice come quello di Python:

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

21/02/2020: In Java 11 ora puoi dire:

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


2
  1. Questo chiamerà add()per ogni membro. Se riesci a trovare un modo più efficiente per inserire elementi in un set di hash, allora usalo. Nota che probabilmente la classe interna genererà immondizia, se sei sensibile a questo.

  2. Mi sembra che il contesto sia l'oggetto restituito da new, che è il HashSet.

  3. Se hai bisogno di chiedere ... Più probabilmente: le persone che vengono dopo di te lo sapranno o no? È facile da capire e spiegare? Se puoi rispondere "sì" ad entrambi, sentiti libero di usarlo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.