¿Qué es la inicialización de Double Brace en Java?

Respuestas:

303

La inicialización de doble llave crea una clase anónima derivada de la clase especificada (las llaves externas ) y proporciona un bloque de inicializador dentro de esa clase (las llaves internas ). p.ej

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

Tenga en cuenta que un efecto del uso de esta inicialización de doble paréntesis es que está creando clases internas anónimas. La clase creada tiene un thispuntero implícito a la clase externa circundante. Aunque normalmente no es un problema, puede causar dolor en algunas circunstancias, por ejemplo, cuando se serializa o se recolecta basura, y vale la pena tenerlo en cuenta.

Brian Agnew
fuente
11
Gracias por aclarar el significado de los frenos internos y externos. Me he preguntado por qué de repente hay dos llaves permitidas con un significado especial, cuando en realidad son construcciones normales de Java que solo aparecen como un nuevo truco mágico. Sin embargo, cosas así me hacen cuestionar la sintaxis de Java. Si aún no eres un experto, puede ser muy difícil de leer y escribir.
jackthehipster
44
La "sintaxis mágica" como esta existe en muchos lenguajes, por ejemplo, casi todos los lenguajes tipo C admiten la sintaxis "va a 0" de "x -> 0" en los bucles, que es simplemente "x--> 0" con extraño Colocación del espacio.
Joachim Sauer el
17
Podemos concluir que la "inicialización de doble paréntesis" no existe por sí sola, es solo una combinación de crear una clase anónima y un bloque inicializador , que, una vez combinado, parece una construcción sintáctica, pero en realidad no lo es. t.
MC Emperor
¡Gracias! Gson devuelve nulo cuando serializamos algo con inicialización de doble paréntesis debido al uso anónimo de la clase interna.
Pradeep AJ- Msft MVP
295

Cada vez que alguien usa la inicialización de doble llave, un gatito es asesinado.

Además de que la sintaxis es bastante inusual y no es realmente idiomática (el sabor es discutible, por supuesto), está creando innecesariamente dos problemas importantes en su aplicación, sobre los que acabo de bloguear recientemente con más detalle aquí .

1. Estás creando demasiadas clases anónimas

Cada vez que utiliza la inicialización de doble paréntesis, se crea una nueva clase. Por ejemplo, este ejemplo:

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

... producirá estas clases:

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

Eso es bastante sobrecarga para tu cargador de clases, ¡para nada! Por supuesto, no tomará mucho tiempo de inicialización si lo hace una vez. Pero si hace esto 20,000 veces en toda su aplicación empresarial ... ¿toda esa memoria de almacenamiento dinámico solo por un poco de "azúcar de sintaxis"?

2. ¡Potencialmente estás creando una pérdida de memoria!

Si toma el código anterior y devuelve ese mapa de un método, las personas que llaman de ese método podrían estar aferrándose a recursos muy pesados ​​que no se pueden recolectar basura. Considere el siguiente ejemplo:

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

El devuelto Mapahora contendrá una referencia a la instancia adjunta de ReallyHeavyObject. Probablemente no quiera arriesgarse a que:

Fuga de memoria aquí mismo

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

3. Puedes fingir que Java tiene literales de mapa

Para responder a su pregunta real, las personas han estado utilizando esta sintaxis para pretender que Java tiene algo así como literales de mapas, similar a los literales de matriz existentes:

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

Algunas personas pueden encontrar esto sintácticamente estimulante.

Lukas Eder
fuente
11
"Estás creando demasiadas clases anónimas" - mirando cómo (por ejemplo) Scala crea clases anónimas, no estoy muy seguro de que este sea un problema importante
Brian Agnew
2
¿No sigue siendo una forma válida y agradable de declarar mapas estáticos? Si un HashMap se inicializa {{...}}y se declara como un staticcampo, no debería haber una posible pérdida de memoria, solo una clase anónima y ninguna referencia de instancia adjunta, ¿verdad?
lorenzo-s
8
@ lorenzo-s: Sí, 2) y 3) no se aplican entonces, solo 1). Afortunadamente, con Java 9, finalmente existe Map.of()para ese propósito, por lo que será una mejor solución
Lukas Eder, el
3
Vale la pena señalar que los mapas internos también tienen referencias a los mapas externos y, por lo tanto, indirectamente a ReallyHeavyObject. Además, las clases internas anónimas capturan todas las variables locales utilizadas dentro del cuerpo de la clase, por lo que si usa no solo constantes para inicializar colecciones o mapas con este patrón, las instancias de la clase interna las capturarán todas y continuarán haciendo referencia a ellas incluso cuando realmente se eliminen de La colección o el mapa. En ese caso, estas instancias no solo necesitan el doble de memoria necesaria para las referencias, sino que tienen otra pérdida de memoria en ese sentido.
Holger
41
  • La primera llave crea una nueva clase interna anónima.
  • El segundo conjunto de llaves crea una instancia de inicializadores como el bloque estático en Class.

Por ejemplo:

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

}

Cómo funciona

La primera llave crea una nueva clase interna anónima. Estas clases internas son capaces de acceder al comportamiento de su clase principal. Entonces, en nuestro caso, en realidad estamos creando una subclase de la clase HashSet, por lo que esta clase interna es capaz de usar el método put ().

Y el segundo conjunto de llaves no es más que inicializadores de instancia. Si recuerda los conceptos básicos de Java, puede asociar fácilmente los bloques de inicializador de instancia con inicializadores estáticos debido a un paréntesis similar como struct. La única diferencia es que el inicializador estático se agrega con una palabra clave estática y se ejecuta solo una vez; no importa cuántos objetos crees.

más

Premraj
fuente
24

Para una aplicación divertida de inicialización de doble paréntesis, vea aquí la matriz de Dwemthy en Java .

Un experto

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

Y ahora, prepárate para el BattleOfGrottoOfSausageSmellsy ... ¡tocino grueso!

akuhn
fuente
16

Creo que es importante enfatizar que no existe la "inicialización de doble paréntesis" en Java . El sitio web de Oracle no tiene este término. En este ejemplo, hay dos características utilizadas juntas: clase anónima y bloque inicializador. Parece que los desarrolladores han olvidado el antiguo bloque inicializador y causan cierta confusión en este tema. Cita de documentos de Oracle :

Los bloques de inicializador para las variables de instancia se parecen a los bloques de inicializador estático, pero sin la palabra clave estática:

{
    // whatever code is needed for initialization goes here
}
Alex T
fuente
11

1- No existe el uso de llaves dobles:
me gustaría señalar que no existe la inicialización de llaves dobles . Solo hay un bloque de inicialización de una llave tradicional tradicional. El segundo bloque de llaves no tiene nada que ver con la inicialización. Las respuestas dicen que esas dos llaves inicializan algo, pero no es así.

2- No se trata solo de clases anónimas sino de todas las clases:
casi todas las respuestas hablan de que es algo que se usa al crear clases internas anónimas. Creo que las personas que leen esas respuestas tendrán la impresión de que esto solo se usa al crear clases internas anónimas. Pero se usa en todas las clases. Parece que leer esas respuestas es una característica especial completamente nueva dedicada a clases anónimas y creo que es engañoso.

3- El propósito es solo colocar los corchetes uno detrás del otro, no es un concepto nuevo:
más allá, esta pregunta habla sobre la situación cuando el segundo corchete de apertura es justo después del primer corchete de apertura. Cuando se usa en la clase normal, generalmente hay algún código entre dos llaves, pero es totalmente lo mismo. Entonces se trata de colocar corchetes. Así que creo que no deberíamos decir que esto es algo nuevo y emocionante, porque es lo que todos sabemos, pero solo escrito con algún código entre paréntesis. No debemos crear un nuevo concepto llamado "inicialización de doble paréntesis".

4- Crear clases anónimas anidadas no tiene nada que ver con dos llaves:
no estoy de acuerdo con el argumento de que creas demasiadas clases anónimas. No los está creando porque es un bloque de inicialización, sino simplemente porque los creó. Se crearían incluso si no utilizara la inicialización de dos llaves, por lo que esos problemas ocurrirían incluso sin inicialización ... La inicialización no es el factor que crea los objetos inicializados.

Además, no deberíamos hablar sobre el problema creado mediante el uso de esta "inicialización de doble paréntesis" inexistente o incluso por la inicialización normal de un paréntesis, porque los problemas descritos solo existen debido a la creación de una clase anónima, por lo que no tiene nada que ver con la pregunta original. Pero todas las respuestas dan a los lectores la impresión de que no es culpa de crear clases anónimas, sino de esta cosa malvada (inexistente) llamada "inicialización de doble paréntesis".

ctomek
fuente
9

Para evitar todos los efectos negativos de la inicialización de doble paréntesis, como:

  1. Roto "igual" compatibilidad.
  2. No se realizan verificaciones, cuando se usan asignaciones directas
  3. Posibles fugas de memoria.

hacer las siguientes cosas:

  1. Haga una clase separada de "Constructor" especialmente para la inicialización de llaves dobles.
  2. Declarar campos con valores predeterminados.
  3. Poner método de creación de objetos en esa clase.

Ejemplo:

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

Ventajas:

  1. Simplemente para usar.
  2. No rompa la compatibilidad "igual".
  3. Puede realizar comprobaciones en el método de creación.
  4. No hay fugas de memoria.

Desventajas

  • Ninguna.

Y, como resultado, tenemos el patrón de generador de Java más simple que existe.

Ver todas las muestras en github: java-sf-builder-simple-example

Boris Podchezertsev
fuente
4

Es, entre otros usos, un atajo para inicializar colecciones. Aprende más ...

miku
fuente
2
Bueno, esa es una aplicación, pero de ninguna manera es la única.
skaffman
4

te refieres a algo como esto?

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

es una inicialización de lista de matriz en tiempo de creación (pirateo)

dhblah
fuente
4

Puede poner algunas declaraciones Java como bucle para inicializar la colección:

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

Pero este caso afecta el rendimiento, verifique esta discusión

Anton Dozortsev
fuente
4

Como se señaló en @Lukas Eder, se debe evitar la doble inicialización de colecciones.

Crea una clase interna anónima, y ​​dado que todas las clases internas mantienen una referencia a la instancia principal, puede, y probablemente el 99% lo hará, evitar la recolección de basura si se hace referencia a estos objetos de recolección por más objetos que solo el declarante.

Java 9 ha introducido métodos de conveniencia List.of, Set.ofy Map.ofque se deben utilizar en su lugar. Son más rápidos y más eficientes que el inicializador de doble paréntesis.

Mikhail Kholodkov
fuente
0

La primera llave crea una nueva clase anónima y el segundo conjunto de llaves crea una instancia de inicializadores como el bloque estático.

Como otros han señalado, no es seguro de usar.

Sin embargo, siempre puede usar esta alternativa para inicializar colecciones.

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

Esto parece ser lo mismo que con la palabra clave tan popular en flash y vbscript. Es un método para cambiar lo que thises y nada más.

Chuck Vose
fuente
Realmente no. Eso sería como decir que crear una nueva clase es un método para cambiar lo que thises. La sintaxis solo crea una clase anónima (por lo que cualquier referencia a thisse referiría al objeto de esa nueva clase anónima), y luego usa un bloque {...}inicializador para inicializar la instancia recién creada.
grinch