Cos'è l'inizializzazione Double Brace in Java?


307

Che cos'è la sintassi di inizializzazione di Double Brace ( {{ ... }}) in Java?




10
L'inizializzazione di Double Brace è una funzione molto pericolosa e deve essere utilizzata con giudizio. Potrebbe rompere il contratto uguale e introdurre perdite di memoria difficili. Questo articolo descrive i dettagli.
Andrii Polunin,

Risposte:


303

L'inizializzazione della doppia parentesi graffa crea una classe anonima derivata dalla classe specificata (le parentesi graffe esterne ) e fornisce un blocco inizializzatore all'interno di quella classe ( parentesi graffe interne ). per esempio

new ArrayList<Integer>() {{
   add(1);
   add(2);
}};

Si noti che un effetto dell'utilizzo di questa inizializzazione a doppia parentesi è che si stanno creando classi interne anonime. La classe creata ha un thispuntatore implicito alla classe esterna circostante. Sebbene normalmente non sia un problema, può causare dolore in alcune circostanze, ad esempio durante la serializzazione o la raccolta dei rifiuti, e vale la pena esserne consapevoli.


11
Grazie per aver chiarito il significato delle parentesi graffe interne ed esterne. Mi sono chiesto perché all'improvviso ci sono due parentesi graffe con un significato speciale, quando in realtà sono normali costrutti Java che appaiono solo come un nuovo trucco magico. Cose del genere mi fanno mettere in dubbio la sintassi Java. Se non sei già un esperto, può essere molto difficile leggere e scrivere.
jackthehipster,

4
"Sintassi magica" come questa esiste in molte lingue, ad esempio quasi tutti i linguaggi C-like supportano la sintassi "va a 0" di "x -> 0" in per i loop che è solo "x--> 0" con strano posizionamento dello spazio.
Joachim Sauer,

17
Possiamo solo concludere che "l'inizializzazione della doppia parentesi" non esiste da sola, è solo una combinazione di creazione di una classe anonima e un blocco di inizializzatore , che, una volta combinati, sembra un costrutto sintattico, ma in realtà non è ' t.
MC Emperor

Grazie! Gson restituisce null quando serializziamo qualcosa con l'inizializzazione a doppia parentesi a causa dell'uso anonimo della classe interna.
Pradeep AJ- Msft MVP,

295

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


11
"Stai creando troppe lezioni anonime" - guardando come (diciamo) Scala crea lezioni anonime, non sono troppo sicuro che questo sia un grosso problema
Brian Agnew

2
Non rimane un modo valido e valido per dichiarare le mappe statiche? Se una HashMap viene inizializzata con {{...}}e dichiarata come staticcampo, non dovrebbe esserci alcuna perdita di memoria possibile, solo una classe anonima e nessun riferimento di istanza incluso, giusto?
lorenzo-s,

8
@ lorenzo-s: Sì, 2) e 3) non si applicano quindi, solo 1). Fortunatamente, con Java 9, finalmente c'è Map.of()quello scopo, quindi sarà una soluzione migliore
Lukas Eder,

3
Vale la pena notare che le mappe interne hanno anche riferimenti alle mappe esterne e quindi, indirettamente, a ReallyHeavyObject. Inoltre, le classi interne anonime catturano tutte le variabili locali utilizzate all'interno del corpo della classe, quindi se si utilizzano non solo costanti per inizializzare raccolte o mappe con questo modello, le istanze della classe interna acquisiranno tutte e continueranno a fare riferimento a esse anche se effettivamente rimosse da la raccolta o la mappa. In tal caso, queste istanze non richiedono solo il doppio della memoria necessaria per i riferimenti, ma presentano un'altra perdita di memoria a tale riguardo.
Holger,

41
  • La prima parentesi crea una nuova classe interna anonima.
  • Il secondo set di parentesi graffe crea un inizializzatore di istanza come blocco statico in Class.

Per esempio:

   public class TestHashMap {
    public static void main(String[] args) {
        HashMap<String,String> map = new HashMap<String,String>(){
        {
            put("1", "ONE");
        }{
            put("2", "TWO");
        }{
            put("3", "THREE");
        }
        };
        Set<String> keySet = map.keySet();
        for (String string : keySet) {
            System.out.println(string+" ->"+map.get(string));
        }
    }

}

Come funziona

Primo tutore crea una nuova classe interna anonima. Queste classi interne sono in grado di accedere al comportamento della loro classe genitore. Quindi, nel nostro caso, stiamo effettivamente creando una sottoclasse della classe HashSet, quindi questa classe interna è in grado di usare il metodo put ().

E il secondo set di parentesi graffe non è altro che inizializzatori di istanza. Se ricordi i concetti java di base, puoi facilmente associare blocchi di inizializzatori di istanze a inizializzatori statici a causa di una struttura simile a parentesi graffe. L'unica differenza è che l'inizializzatore statico viene aggiunto con una parola chiave statica e viene eseguito una sola volta; non importa quanti oggetti crei.

Di Più


24

Per un'applicazione divertente di inizializzazione con doppio controvento, vedere qui Dwemthy's Array in Java .

Un estratto

private static class IndustrialRaverMonkey
  extends Creature.Base {{
    life = 46;
    strength = 35;
    charisma = 91;
    weapon = 2;
  }}

private static class DwarvenAngel
  extends Creature.Base {{
    life = 540;
    strength = 6;
    charisma = 144;
    weapon = 50;
  }}

E ora, preparati per la pancettaBattleOfGrottoOfSausageSmells e ... grosso!


16

Penso che sia importante sottolineare che non esiste qualcosa come "Inizializzazione Double Brace" in Java . Il sito Oracle non ha questo termine. In questo esempio ci sono due funzioni usate insieme: classe anonima e blocco di inizializzatore. Sembra che il vecchio blocco di inizializzazione sia stato dimenticato dagli sviluppatori e causi confusione in questo argomento. Citazione dai documenti Oracle :

I blocchi di inizializzatori per le variabili di istanza sembrano blocchi di inizializzatori statici, ma senza la parola chiave static:

{
    // whatever code is needed for initialization goes here
}

11

1- Non esiste una parentesi graffa doppia:
vorrei sottolineare che non esiste una inizializzazione di parentesi graffe doppie. Esiste solo un normale blocco di inizializzazione di una parentesi graffa tradizionale. Il secondo blocco di parentesi graffe non ha nulla a che fare con l'inizializzazione. Le risposte dicono che quelle due parentesi graffe inizializzano qualcosa, ma non è così.

2- Non si tratta solo di classi anonime ma di tutte le classi:
quasi tutte le risposte dicono che è una cosa usata quando si creano classi interne anonime. Penso che le persone che leggono quelle risposte avranno l'impressione che questo venga usato solo quando si creano classi interne anonime. Ma è usato in tutte le classi. Leggere queste risposte sembra che sia una nuovissima funzione speciale dedicata alle lezioni anonime e penso che sia fuorviante.

3- Lo scopo è solo quello di posizionare le parentesi una dopo l'altra, non un nuovo concetto:
andando oltre, questa domanda parla della situazione in cui la seconda parentesi di apertura è subito dopo la prima parentesi di apertura. Se usato in una classe normale di solito c'è del codice tra due parentesi graffe, ma è totalmente la stessa cosa. Quindi si tratta di posizionare parentesi. Quindi penso che non dovremmo dire che questa è una nuova cosa eccitante, perché questa è la cosa che tutti sappiamo, ma appena scritta con un po 'di codice tra parentesi. Non dovremmo creare un nuovo concetto chiamato "inizializzazione della doppia parentesi graffa".

4- La creazione di classi anonime nidificate non ha nulla a che fare con due parentesi graffe:
non condivido l'argomento secondo cui si creano troppe classi anonime. Non li stai creando perché un blocco di inizializzazione, ma solo perché li crei. Saranno creati anche se non si utilizzassero due inizializzazioni di parentesi graffe, quindi tali problemi si verificherebbero anche senza inizializzazione ... L'inizializzazione non è il fattore che crea oggetti inizializzati.

Inoltre, non dovremmo parlare del problema creato utilizzando questa cosa inesistente "inizializzazione a doppia parentesi" o anche dalla normale inizializzazione a una parentesi, perché i problemi descritti esistono solo a causa della creazione di una classe anonima, quindi non ha nulla a che fare con la domanda originale. Ma tutte le risposte danno ai lettori l'impressione che non sia colpa della creazione di classi anonime, ma questa cosa malvagia (inesistente) chiamata "inizializzazione a doppia coppia".


9

Per evitare tutti gli effetti negativi dell'inizializzazione del doppio controvento, come ad esempio:

  1. Compatibilità "uguale" interrotta.
  2. Nessun controllo eseguito quando si utilizzano assegnazioni dirette.
  3. Possibili perdite di memoria.

fai le prossime cose:

  1. Crea una classe "Builder" separata in particolare per l'inizializzazione della doppia parentesi graffa.
  2. Dichiarare i campi con valori predefiniti.
  3. Inserisci il metodo di creazione dell'oggetto in quella classe.

Esempio:

public class MyClass {
    public static class Builder {
        public int    first  = -1        ;
        public double second = Double.NaN;
        public String third  = null      ;

        public MyClass create() {
            return new MyClass(first, second, third);
        }
    }

    protected final int    first ;
    protected final double second;
    protected final String third ;

    protected MyClass(
        int    first ,
        double second,
        String third
    ) {
        this.first = first ;
        this.second= second;
        this.third = third ;
    }

    public int    first () { return first ; }
    public double second() { return second; }
    public String third () { return third ; }
}

Uso:

MyClass my = new MyClass.Builder(){{ first = 1; third = "3"; }}.create();

vantaggi:

  1. Semplicemente da usare.
  2. Non rompe la compatibilità "uguale".
  3. È possibile eseguire controlli nel metodo di creazione.
  4. Nessuna perdita di memoria.

svantaggi:

  • Nessuna.

E, di conseguenza, abbiamo il modello java builder più semplice di sempre.

Guarda tutti gli esempi su github: java-sf-builder-simple-example



4

intendi qualcosa del genere?

List<String> blah = new ArrayList<String>(){{add("asdfa");add("bbb");}};

è un'inizializzazione dell'elenco di array in fase di creazione (hack)


4

È possibile inserire alcune istruzioni Java come loop per inizializzare la raccolta:

List<Character> characters = new ArrayList<Character>() {
    {
        for (char c = 'A'; c <= 'E'; c++) add(c);
    }
};

Random rnd = new Random();

List<Integer> integers = new ArrayList<Integer>() {
    {
         while (size() < 10) add(rnd.nextInt(1_000_000));
    }
};

Ma questo caso influisce sulle prestazioni, controlla questa discussione


4

Come sottolineato da @Lukas, Eder deve evitare l'inizializzazione delle doppie parentesi graffe.

Crea una classe interna anonima e poiché tutte le classi interne mantengono un riferimento all'istanza padre può - e probabilmente il 99% lo eviterà - impedire la garbage collection se questi oggetti della raccolta sono referenziati da più oggetti oltre a quello dichiarante.

Java 9 ha introdotto metodi di convenienza List.of, Set.ofe Map.of, che dovrebbero essere utilizzati al posto. Sono più veloci ed efficienti dell'inizializzatore a doppia parentesi.


0

La prima parentesi graffa crea una nuova classe anonima e la seconda serie di controventi crea un inizializzatore di istanza come il blocco statico.

Come altri hanno sottolineato, non è sicuro da usare.

Tuttavia, puoi sempre utilizzare questa alternativa per inizializzare le raccolte.

  • Java 8
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
  • Java 9
List<String> list = List.of("A", "B", "C");

-1

Questo sembrerebbe essere lo stesso della parola chiave with così popolare in flash e vbscript. È un metodo per cambiare ciò che thisè e niente di più.


Non proprio. Sarebbe come dire che creare una nuova classe è un metodo per cambiare ciò che thisè. La sintassi crea solo una classe anonima (quindi qualsiasi riferimento a thisfarebbe riferimento all'oggetto di quella nuova classe anonima), quindi utilizza un blocco {...}di inizializzatore per inizializzare l'istanza appena creata.
grinch,
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.