initialize - lazy initialization java




¿Eficiencia de la Java “Double Brace Initialization”? (10)

La inicialización de doble refuerzo es un truco innecesario que puede introducir fugas de memoria y otros problemas

No hay una razón legítima para usar este "truco". Guava proporciona colecciones inmutables que incluyen tanto fábricas estáticas como constructores, lo que le permite llenar su colección donde se declara en una sintaxis limpia, legible y segura .

El ejemplo en la pregunta se convierte en:

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

No solo es más corto y fácil de leer, sino que evita los numerosos problemas con el patrón de doble refuerzo descrito en otras respuestas . Claro, su desempeño es similar al de construcción directa HashMap, pero es peligroso y propenso a errores, y hay mejores opciones.

Cada vez que considere la posibilidad de realizar una inicialización con doble refuerzo, debe volver a examinar sus API o introducir otras nuevas para abordar el problema adecuadamente, en lugar de aprovechar los trucos sintácticos.

Error-Prone ahora marca este anti-patrón .

En Hidden Features of Java, la respuesta principal menciona la inicialización de doble refuerzo , con una sintaxis muy atractiva:

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

Este idioma crea una clase interna anónima con solo un inicializador de instancia, que "puede usar cualquier [...] método en el ámbito que lo contiene".

Pregunta principal: ¿Es esto tan ineficiente como parece? ¿Debería su uso estar limitado a inicializaciones únicas? (Y por supuesto presumiendo!)

Segunda pregunta: el nuevo HashSet debe ser el "esto" usado en el inicializador de instancia ... ¿alguien puede arrojar luz sobre el mecanismo?

Tercera pregunta: ¿Es este lenguaje demasiado oscuro para usar en el código de producción?

Resumen: Muy, muy buenas respuestas, gracias a todos. En la pregunta (3), la gente sintió que la sintaxis debería ser clara (aunque recomendaría un comentario ocasional, especialmente si su código se transmite a los desarrolladores que no estén familiarizados con él).

En la pregunta (1), el código generado debe ejecutarse rápidamente. Los archivos .class adicionales causan el desorden de archivos jar, y retrasan ligeramente el inicio del programa (gracias a @coobird por medirlo). @Thilo señaló que la recolección de basura puede verse afectada, y el costo de memoria para las clases con carga adicional puede ser un factor en algunos casos.

La pregunta (2) resultó ser la más interesante para mí. Si entiendo las respuestas, lo que está sucediendo en DBI es que la clase interna anónima extiende la clase del objeto que está construyendo el nuevo operador, y por lo tanto tiene un valor "this" que hace referencia a la instancia que se está construyendo. Muy aseado.

En general, DBI me parece una especie de curiosidad intelectual. Coobird y otros señalan que puede lograr el mismo efecto con Arrays.asList, los métodos de varargs, Google Collections y los literales propuestos de Java 7 Collection. Los nuevos lenguajes JVM como Scala, JRuby y Groovy también ofrecen notaciones concisas para la construcción de listas, e interoperan bien con Java. Dado que el DBI desordena el classpath, ralentiza la carga de la clase un poco y hace que el código sea un poco más oscuro, probablemente lo evito. Sin embargo, tengo la intención de explicar esto a un amigo que acaba de obtener su SCJP y que ama las buenas intenciones sobre la semántica de Java. ;-) ¡Gracias a todos!

7/2017: Baeldung tiene un buen resumen de la inicialización de doble refuerzo y lo considera un antipatrón.

12/2017: @Basil Bourque señala que en el nuevo Java 9 puede decir:

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

Eso es seguro el camino a seguir. Si está atascado con una versión anterior, eche un vistazo a ImmutableSet de Google Collections .


  1. Esto llamará add()a cada miembro. Si puedes encontrar una forma más eficiente de colocar elementos en un conjunto hash, entonces úsalo. Tenga en cuenta que la clase interna probablemente generará basura, si es sensible al respecto.

  2. Me parece que el contexto es el objeto devuelto por new, que es el HashSet.

  3. Si necesita preguntar ... Lo más probable es que las personas que lo buscan lo sepan o no. ¿Es fácil de entender y explicar? Si puede responder "sí" a ambos, siéntase libre de usarlo.


Cargar muchas clases puede agregar algunos milisegundos al inicio. Si el inicio no es tan crítico y se observa la eficiencia de las clases después del inicio, no hay diferencia.

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

huellas dactilares

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

Dejando de lado la eficiencia, rara vez me encuentro deseando crear una colección declarativa fuera de las pruebas unitarias. Creo que la sintaxis de doble llave es muy legible.

Otra forma de lograr la construcción declarativa de listas específicamente es usar Arrays.asList(T ...) así:

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

La limitación de este enfoque es, por supuesto, que no puede controlar el tipo específico de lista que se generará.


Estaba investigando esto y decidí hacer una prueba más profunda que la proporcionada por la respuesta válida.

Aquí está el código: https://gist.github.com/4368924

y esta es mi conclusión

Me sorprendió descubrir que en la mayoría de las pruebas de ejecución, la iniciación interna era en realidad más rápida (casi el doble en algunos casos). Cuando se trabaja con grandes números, el beneficio parece desaparecer.

Curiosamente, el caso que crea 3 objetos en el bucle pierde su beneficio se ransea antes que en los otros casos. No estoy seguro de por qué sucede esto y se deben realizar más pruebas para llegar a alguna conclusión. Crear implementaciones concretas puede ayudar a evitar la recarga de la definición de clase (si eso es lo que está sucediendo)

Sin embargo, está claro que no se observaron muchos gastos generales en la mayoría de los casos para la construcción de un solo elemento, incluso con grandes números.

Un retroceso sería el hecho de que cada una de las iniciaciones de refuerzo doble crea un nuevo archivo de clase que agrega un bloque de disco completo al tamaño de nuestra aplicación (o aproximadamente 1k cuando se comprime). Una huella pequeña, pero si se usa en muchos lugares, podría tener un impacto. Use esto 1000 veces y potencialmente está agregando un MiB completo a su aplicación, lo que puede ser relevante en un entorno integrado.

¿Mi conclusión? Puede estar bien usarlo siempre que no se abuse de él.

Déjame saber lo que piensas :)


Para crear conjuntos, puede utilizar un método de fábrica de varargs en lugar de una inicialización de doble refuerzo:

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

La biblioteca de Google Collections tiene muchos métodos de conveniencia como este, así como un montón de otras funcionalidades útiles.

En cuanto a la oscuridad del idioma, lo encuentro y lo uso en el código de producción todo el tiempo. Estaría más preocupado por los programadores que se confunden por el lenguaje que se le permite escribir código de producción.


Si bien esta sintaxis puede ser conveniente, también agrega muchas de estas referencias de $ 0 a medida que se anidan y puede ser difícil realizar una depuración en los inicializadores a menos que se establezcan puntos de interrupción en cada uno. Por esa razón, solo recomiendo usar esto para los definidores banales, especialmente para las constantes, y los lugares donde no importan las subclases anónimas (como ninguna serialización involucrada).


Tomando la siguiente clase de prueba:

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

y luego descompilar el archivo de la clase, veo:

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

Esto no me parece terriblemente ineficiente. Si estuviera preocupado por el rendimiento de algo como esto, lo perfilaría. Y su pregunta # 2 es respondida por el código anterior: Usted está dentro de un constructor implícito (y un inicializador de instancia) para su clase interna, por lo que " this " se refiere a esta clase interna.

Sí, esta sintaxis es oscura, pero un comentario puede aclarar el uso de la sintaxis oscura. Para aclarar la sintaxis, la mayoría de las personas están familiarizadas con un bloque de inicializador estático (Inicializadores estáticos JLS 8.7):

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

También puede usar una sintaxis similar (sin la palabra " static ") para el uso del constructor (Inicializadores de instancia JLS 8.6), aunque nunca he visto esto usado en el código de producción. Esto es mucho menos conocido.

public class Sample2 {
    private final String someVar;

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

Si no tiene un constructor predeterminado, el compilador convierte el bloque de código entre { y } en un constructor. Con esto en mente, desenrede el código de doble corsé:

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

El bloque de código entre las llaves más internas se convierte en un constructor por el compilador. Las llaves más externas delimitan la clase interna anónima. Para tomar este paso final de hacer todo lo que no sea 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 propósitos de inicialización, diría que no hay gastos generales (o tan pequeños que se puedan descuidar). Sin embargo, todo uso de flavors no irá contra HashSet sino contra MyHashSet . Probablemente hay una pequeña sobrecarga (y, posiblemente, insignificante) para esto. Pero una vez más, antes de que me preocupe, lo perfilaría.

Nuevamente, para su pregunta # 2, el código anterior es el equivalente lógico y explícito de la inicialización de doble refuerzo, y hace que sea obvio donde " this " se refiere: a la clase interna que extiende HashSet .

Si tiene preguntas sobre los detalles de los inicializadores de instancias, consulte los detalles en la documentación de JLS .


Cada vez que alguien usa la inicialización de doble refuerzo, un gatito muere.

Aparte de que la sintaxis es bastante inusual y no es realmente idiomática (el sabor es discutible, por supuesto), está creando innecesariamente dos problemas significativos en su aplicación, que recientemente he publicado en el blog con más detalle aquí .

1. Estás creando demasiadas clases anónimas

Cada vez que se utiliza la inicialización de doble refuerzo 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 un poco de sobrecarga para su cargador de clases, ¡para nada! Por supuesto, no tomará mucho tiempo de inicialización si lo haces una vez. Pero si lo hace 20'000 veces en su aplicación empresarial ... ¿todo ese montón de memoria solo por un poco de "sintaxis de azúcar"?

2. ¡Estás creando potencialmente una pérdida de memoria!

Si toma el código anterior y devuelve ese mapa desde un método, las personas que llaman a ese método podrían estar reteniendo sin sospecha recursos muy pesados ​​que no pueden ser recolectados como 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 Map devuelto ahora contendrá una referencia a la instancia ReallyHeavyObject de ReallyHeavyObject . Probablemente no quieras arriesgarte a que:

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

3. Puedes pretender que Java tiene literales de mapas.

Para responder a su pregunta real, las personas han estado usando esta sintaxis para pretender que Java tiene algo así como los 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.


propenso a fugas

Decidí participar. El impacto en el rendimiento incluye: operación del disco + descomprimir (para jarra), verificación de clase, espacio perm-gen (para JVM de punto de acceso de Sun). Sin embargo, lo peor de todo: es propenso a fugas. No puedes simplemente regresar.

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

Entonces, si el conjunto escapa a cualquier otra parte cargada por un cargador de clases diferente y se guarda una referencia allí, se filtrará todo el árbol de clases + el cargador de clases. Para evitar eso, es necesaria una copia a HashMap, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}) . Ya no es tan lindo. No uso el idioma , yo, en cambio, es como new LinkedHashSet(Arrays.asList("xxx","YYY"));







initialization