Eficiencia de Java "Double Brace Initialization"?

824

En Hidden Features of Java, la respuesta principal menciona Double Brace Initialization , 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 de contención".

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

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

Tercera pregunta: ¿Es este idioma 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 pasará a los desarrolladores que pueden no estar familiarizados con él).

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

La pregunta (2) resultó ser la más interesante para mí. Si entiendo las respuestas, lo que sucede 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 "este" valor 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, métodos varargs, Google Collections y los literales propuestos para la Colección Java 7. Los nuevos lenguajes JVM como Scala, JRuby y Groovy también ofrecen anotaciones concisas para la construcción de listas, e interoperan bien con Java. Dado que DBI abarrota el classpath, ralentiza un poco la carga de la clase y hace que el código sea un poco más oscuro, probablemente lo evitaría. ¡Sin embargo, planeo lanzar esto a un amigo que acaba de obtener su SCJP y ama las justas bondadosas sobre la semántica de Java! ;-) ¡Gracias a todos!

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

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

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

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

Jim Ferrans
fuente
33
El olor a código que veo aquí es que el lector ingenuo esperaría flavorsser un HashSet, pero lamentablemente es una subclase anónima.
Elazar Leibovich
66
Si considera ejecutar en lugar de cargar el rendimiento, no hay diferencia, vea mi respuesta.
Peter Lawrey
44
Me encanta que hayas creado un resumen, creo que este es un ejercicio que vale la pena tanto para aumentar la comprensión como para la comunidad.
Patrick Murphy
3
No es oscuro en mi opinión. Los lectores deben saber que un doble ... o espera, @ElazarLeibovich ya lo dijo en su comentario . El inicializador de doble paréntesis en sí no existe como una construcción de lenguaje, es solo una combinación de una subclase anónima y un inicializador de instancia. Lo único es que las personas deben ser conscientes de esto.
MC Emperor
8
Java 9 ofrece métodos de fábrica estáticos de conjuntos inmutables que pueden reemplazar el uso de DCI en algunas situaciones:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque

Respuestas:

607

Aquí está el problema cuando me dejo llevar por las clases 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

Estas son todas las clases que se generaron cuando estaba haciendo una aplicación simple, y utilicé grandes cantidades de clases internas anónimas: cada clase se compilará en un classarchivo separado .

La "inicialización de doble paréntesis", como ya se mencionó, es una clase interna anónima con un bloque de inicialización de instancia, lo que significa que se crea una nueva clase para cada "inicialización", todo con el propósito de hacer generalmente un solo objeto.

Teniendo en cuenta que Java Virtual Machine necesitará leer todas esas clases cuando las use, eso puede llevar algún tiempo en el proceso de verificación de bytecode y tal. Sin mencionar el aumento en el espacio en disco necesario para almacenar todos esosclass archivos.

Parece que hay un poco de sobrecarga cuando se utiliza la inicialización de doble paréntesis, por lo que probablemente no sea una buena idea ir demasiado por la borda. Pero como ha señalado Eddie en los comentarios, no es posible estar absolutamente seguro del impacto.


Solo como referencia, la inicialización de doble llave es la siguiente:

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

Parece una característica "oculta" de Java, pero es solo una reescritura de:

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

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

Entonces, es básicamente un bloque de inicialización de instancia que es parte de una clase interna anónima .


La propuesta de Joshua Bloch's Collection Literals para Project Coin fue similar a :

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

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

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

Lamentablemente, no llegó a Java 7 ni 8 y se archivó indefinidamente.


Experimentar

Aquí está el experimento simple que probé: crea 1000 ArrayLists con los elementos "Hello"y los "World!"agrega a través del addmétodo, usando los dos métodos:

Método 1: inicialización de doble llave

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

Método 2: crear una instancia de ArrayListyadd

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

Creé un programa simple para escribir un archivo fuente Java para realizar 1000 inicializaciones usando los dos métodos:

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

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

Tenga en cuenta que el tiempo transcurrido para inicializar la ArrayListextensión de 1000 sy 1000 clases anónimas internas ArrayListse verifica utilizando elSystem.currentTimeMillis , por lo que el temporizador no tiene una resolución muy alta. En mi sistema Windows, la resolución es de alrededor de 15-16 milisegundos.

Los resultados para 10 ejecuciones de las dos pruebas fueron los siguientes:

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 se puede ver, la inicialización de doble llave tiene un tiempo de ejecución notable de alrededor de 190 ms.

Mientras tanto, el ArrayListtiempo de ejecución de la inicialización resultó ser de 0 ms. Por supuesto, la resolución del temporizador debe tenerse en cuenta, pero es probable que sea inferior a 15 ms.

Entonces, parece haber una diferencia notable en el tiempo de ejecución de los dos métodos. Parece que de hecho hay algo de sobrecarga en los dos métodos de inicialización.

Y sí, se .classgeneraron 1000 archivos al compilar el Test1programa de prueba de inicialización de doble paréntesis.

Coobird
fuente
10
"Probablemente" es la palabra clave. A menos que se mida, ninguna declaración sobre el rendimiento es significativa.
Instancia Hunter
16
Has hecho un trabajo tan bueno que apenas quiero decir esto, pero los tiempos de Test1 podrían estar dominados por cargas de clase. Sería interesante ver a alguien ejecutar una sola instancia de cada prueba en un ciclo for, digamos 1,000 veces, luego ejecutarlo nuevamente en un segundo ciclo for de 1,000 o 10,000 veces e imprimir la diferencia horaria (System.nanoTime ()). El primer bucle for debería superar todos los efectos de calentamiento (JIT, carga de clase, por ejemplo). Sin embargo, ambas pruebas modelan diferentes casos de uso. Trataré de ejecutar esto mañana en el trabajo.
Jim Ferrans
8
@ Jim Ferrans: Estoy bastante seguro de que los tiempos de Test1 son de cargas de clase. Pero, la consecuencia del uso de la inicialización de doble paréntesis es tener que hacer frente a las cargas de clase. Creo que la mayoría de los casos de uso para la doble llave de inicio. es para una inicialización única, la prueba está más cerca en condiciones de un caso de uso típico de este tipo de inicialización. Creo que múltiples iteraciones de cada prueba reducirían la brecha de tiempo de ejecución.
Coobird
73
Lo que esto prueba es que a) la inicialización de doble refuerzo es más lenta, yb) incluso si lo hace 1000 veces, probablemente no notará la diferencia. Y tampoco es que esto pueda ser el cuello de botella en un circuito interno. Impone una pequeña pena de una sola vez EN LO PEOR.
Michael Myers
15
Si usar DBI hace que el código sea más legible o expresivo, entonces úselo. El hecho de que aumente un poco el trabajo que debe realizar la JVM no es un argumento válido, en sí mismo, en su contra. Si lo fuera, entonces debemos también estar preocupado por helper métodos / clases adicionales, prefiriendo en lugar de grandes clases con un menor número de métodos ...
Rogério
105

Una propiedad de este enfoque que no se ha señalado hasta ahora es que debido a que crea clases internas, toda la clase que contiene se captura en su alcance. Esto significa que mientras su Set esté vivo, retendrá un puntero a la instancia que lo contiene ( this$0) y evitará que se recolecte basura, lo que podría ser un problema.

Esto, y el hecho de que se cree una nueva clase en primer lugar a pesar de que un HashSet normal funcionaría bien (o incluso mejor), hace que no quiera usar esta construcción (aunque realmente anhelo el azúcar sintáctico).

Segunda pregunta: El nuevo HashSet debe ser el "this" utilizado en el inicializador de instancia ... ¿alguien puede arrojar luz sobre el mecanismo? Ingenuamente esperaba que "esto" se refiriera al objeto que inicializa "sabores".

Así es como funcionan las clases internas. Obtienen los suyos this, pero también tienen punteros a la instancia principal, por lo que también puede invocar métodos en el objeto que los contiene. En caso de un conflicto de nomenclatura, la clase interna (en su caso HashSet) tiene prioridad, pero puede prefijar "esto" con un nombre de clase para obtener también el 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 ser claro sobre la subclase anónima que se está creando, también puede definir métodos allí. Por ejemplo, anularHashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }
Thilo
fuente
55
Muy buen punto sobre la referencia oculta a la clase que lo contiene. En el ejemplo original, el inicializador de instancia está llamando al método add () del nuevo HashSet <String>, no Test.this.add (). Eso me sugiere que algo más está sucediendo. ¿Existe una clase interna anónima para el HashSet <String>, como sugiere Nathan Kitchen?
Jim Ferrans
La referencia a la clase que lo contiene también podría ser peligrosa si está involucrada la serialización de la estructura de datos. La clase que contiene también se serializará y, por lo tanto, debe ser serializable. Esto puede conducir a errores oscuros.
Solo otro programador de Java
56

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 llave 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 "sintaxis de azúcar"?

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 Mapcontendrá ahora 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. Puede pretender que Java tiene literales de mapa

Para responder a su pregunta real, la gente ha estado usando 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
77
¡Salva a los gatitos! ¡Buena respuesta!
Aris2World
36

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 descompilando el archivo de 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 me preocupara el rendimiento de algo como esto, lo perfilaría. Y su pregunta # 2 es respondida por el código anterior: está dentro de un constructor implícito (e 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 oscuro de la sintaxis. Para aclarar la sintaxis, la mayoría de las personas están familiarizadas con un bloque de inicializador estático (JLS 8.7 Static Initializer):

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 }. Con esto en mente, desentraña el código de doble llave:

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 dar este paso final, hacer que todo sea no 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 fines de inicialización, diría que no hay gastos generales (o tan pequeños que se pueden descuidar). Sin embargo, cada uso de flavorsno irá en contraHashSet sino en contra MyHashSet. Probablemente hay una pequeña sobrecarga (y posiblemente insignificante) para esto. Pero de nuevo, antes de preocuparme por eso, lo perfilaría.

Nuevamente, a su pregunta # 2, el código anterior es el equivalente lógico y explícito de la inicialización de doble paréntesis, y hace obvio dónde " this" se refiere: a la clase interna que se extiendeHashSet .

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

Eddie
fuente
Eddie, muy buena explicación. Si los códigos de bytes JVM son tan limpios como la descompilación, la velocidad de ejecución será lo suficientemente rápida, aunque me preocuparía un poco el desorden de archivos .class extra. Todavía tengo curiosidad por saber por qué el constructor del inicializador de instancia ve "esto" como la nueva instancia de HashSet <String> y no como la instancia de Prueba. ¿Es este comportamiento explícitamente especificado en la última especificación del lenguaje Java para admitir el idioma?
Jim Ferrans
Actualicé mi respuesta. Dejé fuera la plantilla de la clase Test, lo que causó la confusión. Lo puse en mi respuesta para hacer las cosas más obvias. También menciono la sección JLS para los bloques de inicializador de instancia utilizados en este idioma.
Eddie
1
@ Jim La interpretación de "esto" no es un caso especial; simplemente se refiere a la instancia de la clase de encierro más interna, que es la subclase anónima de HashSet <String>.
Nathan Kitchen
Lamento saltar cuatro años y medio después. ¡Pero lo bueno del archivo de clase descompilado (su segundo bloque de código) es que no es Java válido! Tiene super()como segunda línea del constructor implícito, pero tiene que venir primero. (Lo he probado y no se compilará.)
chiastic-security
1
@ chiastic-security: a veces los descompiladores generan código que no se compilará.
Eddie
35

propenso a fugas

He decidido intervenir. El impacto en el rendimiento incluye: operación de disco + descompresión (para jar), verificación de clase, espacio permanente (para JVM Hotspot 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 se escapa a cualquier otra parte cargada por un cargador de clases diferente y se mantiene una referencia allí, se filtrará todo el árbol de clases + cargador de clases. Para evitar que, una copia a HashMap es necesario, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Ya no es tan lindo. Yo no uso el idioma, en cambio es como new LinkedHashSet(Arrays.asList("xxx","YYY"));

vendidos
fuente
3
Afortunadamente, a partir de Java 8, PermGen ya no es una cosa. Todavía hay un impacto, supongo, pero no uno con un mensaje de error potencialmente muy oscuro.
Joey
2
@Joey, no hace ninguna diferencia si la memoria es administrada directamente por el GC (gen permanente) o no. Una fuga en metaespacio todavía existe una fuga, a menos que se limita meta no habrá un OOM (de la ondulación permanente gen) por cosas como oom_killer en Linux va a patada en.
bestsss
19

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
Peter Lawrey
fuente
2
No hay diferencia, excepto que su espacio PermGen se evaporará si DBI se usa en exceso. Al menos, lo hará a menos que establezca algunas opciones de JVM oscuras para permitir la descarga de clases y la recolección de basura del espacio PermGen. Dada la prevalencia de Java como lenguaje del lado del servidor, el problema de memoria / PermGen garantiza al menos una mención.
Aroth
1
@ Aroth este es un buen punto. Admito que en 16 años de trabajo en Java nunca he trabajado en un sistema en el que haya que ajustar PermGen (o Metaspace). Para los sistemas en los que he trabajado, el tamaño del código siempre se mantuvo razonablemente pequeño.
Peter Lawrey
2
¿No deberían compareCollectionscombinarse las condiciones en ||lugar de hacerlo &&? El uso &&no solo parece semánticamente incorrecto, sino que contrarresta la intención de medir el rendimiento, ya que solo se probará la primera condición. Además, un optimizador inteligente puede reconocer que las condiciones nunca cambiarán durante las iteraciones.
Holger
@aroth solo como una actualización: desde Java 8, la VM ya no usa ningún perm-gen.
Angel O'Sphere
16

Para crear conjuntos, puede usar un método de fábrica de varargs en lugar de la inicialización de doble llave:

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

La biblioteca de Google Collections tiene muchos métodos convenientes como este, así como muchas 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 con el idioma que se les permite escribir código de producción.

Nat
fuente
Ja! ;-) En realidad soy un Rip van Winkle que regresa a Java desde los 1.2 días (escribí el navegador web de voz VoiceXML en evolution.voxeo.com en Java). Ha sido divertido aprender genéricos, tipos parametrizados, Colecciones, java.util.concurrent, lo nuevo para la sintaxis de bucles, etc. Ahora es un lenguaje mejor. Según su punto de vista, aunque el mecanismo detrás de DBI pueda parecer oscuro al principio, el significado del código debería ser bastante claro.
Jim Ferrans
10

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 paréntesis 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á.

Paul Morie
fuente
1
Arrays.asList () es lo que normalmente usaría, pero tienes razón, esta situación surge principalmente en pruebas unitarias; el código real construiría las listas a partir de consultas DB, XML, etc.
Jim Ferrans
77
Sin embargo, tenga cuidado con asList: la lista devuelta no admite agregar o quitar elementos. Cada vez que uso asList, paso la lista resultante a un constructor new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))para solucionar este problema.
Michael Myers
7

Generalmente no hay nada particularmente ineficiente al respecto. Por lo general, a la JVM no le importa que haya hecho una subclase y le haya agregado un constructor; eso es algo normal y cotidiano en un lenguaje orientado a objetos. Puedo pensar en casos bastante ingeniosos en los que podrías causar una ineficiencia al hacer esto (por ejemplo, tienes un método repetidamente llamado que termina tomando una mezcla de diferentes clases debido a esta subclase, mientras que la clase normal que se pasa sería totalmente predecible) - en el último caso, el compilador JIT podría hacer optimizaciones que no son factibles en el primero). Pero realmente, creo que los casos en los que importará son muy artificiales.

Vería el problema más desde el punto de vista de si desea "desordenar las cosas" con muchas clases anónimas. Como una guía aproximada, considere usar el idioma no más de lo que usaría, por ejemplo, clases anónimas para controladores de eventos.

En (2), estás dentro del constructor de un objeto, por lo que "esto" se refiere al objeto que estás construyendo. Eso no es diferente a cualquier otro constructor.

En cuanto a (3), eso realmente depende de quién mantenga su código, supongo. Si no sabe esto de antemano, entonces un punto de referencia que sugeriría usar es "¿ve esto en el código fuente del JDK?" (en este caso, no recuerdo haber visto muchos inicializadores anónimos, y ciertamente no en los casos en que ese es el único contenido de la clase anónima). En la mayoría de los proyectos de tamaño moderado, diría que realmente necesitará que sus programadores comprendan la fuente JDK en algún momento u otro, por lo que cualquier sintaxis o modismo utilizado allí es un "juego justo". Más allá de eso, diría, capacite a las personas en esa sintaxis si tiene control sobre quién mantiene el código, de lo contrario, comente o evite.

Neil Coffey
fuente
5

La inicialización de doble llave es un hack innecesario que puede introducir fugas de memoria y otros problemas.

No hay una razón legítima para usar este "truco". Guava proporciona colecciones agradables e inmutables que incluyen tanto fábricas estáticas como constructores, lo que le permite poblar 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");

Esto 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, funciona de manera similar a un construido directamente HashMap, pero es peligroso y propenso a errores, y hay mejores opciones.

Cada vez que se encuentre considerando una inicialización de doble refuerzo, debe volver a examinar sus API o introducir nuevas. para abordar adecuadamente el problema, en lugar de aprovechar los trucos sintácticos.

Propenso a errores ahora marca este antipatrón .

dimo414
fuente
-1. A pesar de algunos puntos válidos, esta respuesta se reduce a "¿Cómo evitar generar clases anónimas innecesarias? ¡Use un marco, con incluso más clases!"
Agent_L
1
Yo diría que se reduce a "usar la herramienta adecuada para el trabajo, en lugar de un truco que puede bloquear su aplicación". Guava es una biblioteca bastante común para incluir aplicaciones (definitivamente te estás perdiendo si no la estás usando), pero incluso si no quieres usarla puedes y debes evitar la inicialización de doble llave.
dimo414
¿Y cómo exactamente una inicialización de doble llave podría causar una fuga de memoria?
Angel O'Sphere
@ AngelO'Sphere DBI es una forma ofuscada de crear una clase interna y, por lo tanto, conserva una referencia implícita a su clase adjunta (a menos que solo se use en staticcontextos). El enlace propenso a errores en la parte inferior de mi pregunta discute esto más a fondo.
dimo414
Yo diría que es cuestión de gustos. Y no hay nada realmente ofuscante al respecto.
Angel O'Sphere
4

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 fue realmente más rápida (casi el doble en algunos casos). Cuando se trabaja con grandes cantidades, el beneficio parece desvanecerse.

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

Sin embargo, está claro que no se observó mucha sobrecarga 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 doble llave crea un nuevo archivo de clase que agrega un bloque de disco completo al tamaño de nuestra aplicación (o aproximadamente 1k cuando está comprimido). Una huella pequeña, pero si se usa en muchos lugares, podría tener un impacto potencial. Use esto 1000 veces y posiblemente esté agregando un MiB completo a su aplicación, lo que puede ser preocupante en un entorno integrado.

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

Déjame saber lo que piensas :)

pablisco
fuente
2
Esa no es una prueba válida. El código crea objetos sin usarlos, lo que permite al optimizador eludir toda la creación de la instancia. El único efecto secundario restante es el avance de la secuencia de números aleatorios cuya sobrecarga supera cualquier otra cosa en estas pruebas de todos modos.
Holger
3

Secundo la respuesta de Nat, excepto que usaría un bucle en lugar de crear e inmediatamente tirar la 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
fuente
1
¿Por qué? El nuevo objeto se creará en el espacio edén, por lo que solo se requieren dos o tres adiciones de puntero para crear una instancia. La JVM puede notar que nunca escapa más allá del alcance del método y así asignarlo en la pila.
Nat
Sí, es probable que termine más eficiente que ese código (aunque puede mejorarlo al indicar la HashSetcapacidad sugerida: recuerde el factor de carga).
Tom Hawtin - tackline
Bueno, el constructor HashSet tiene que hacer la iteración de todos modos, por lo que no será menos eficiente. El código de biblioteca creado para su reutilización siempre debe esforzarse por ser el mejor posible.
Lawrence Dol
3

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 depurar 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 creadores banales, especialmente para las constantes, y los lugares donde las subclases anónimas no importan (como no hay serialización involucrada).

Eric Woodruff
fuente
3

Mario Gleichman describe cómo usar las funciones genéricas de Java 1.5 para simular literales de Scala List, aunque lamentablemente terminas con inmutable listas .

Él define esta clase:

package literal;

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

y lo usa así:

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, ahora parte de Guava, respalda una idea similar para la construcción de listas. En esta entrevista , Jared Levy dice:

[...] las características más utilizadas, que aparecen en casi todas las clases de Java que escribo, son métodos estáticos que reducen la cantidad de pulsaciones de teclas repetitivas en su código Java. Es muy conveniente poder ingresar comandos como los siguientes:

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

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

10/07/2014: Si tan solo pudiera ser tan simple como el de Python:

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

21/02/2020: en Java 11 ahora puedes decir:

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

Jim Ferrans
fuente
2
  1. Esto llamará add()a cada miembro. Si puede encontrar una forma más eficiente de colocar elementos en un conjunto hash, entonces úselo. 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 ... Más probablemente: ¿las personas que vienen después de usted lo saben o no? ¿Es fácil de entender y explicar? Si puede responder "sí" a ambas, siéntase libre de usarlo.

MC Emperor
fuente