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 .
fuente
flavors
ser unHashSet
, pero lamentablemente es una subclase anónima.Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Respuestas:
Aquí está el problema cuando me dejo llevar por las clases internas anónimas:
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
class
archivo 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 esos
class
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:
Parece una característica "oculta" de Java, pero es solo una reescritura de:
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 :
Lamentablemente, no llegó a Java 7 ni 8 y se archivó indefinidamente.
Experimentar
Aquí está el experimento simple que probé: crea 1000
ArrayList
s con los elementos"Hello"
y los"World!"
agrega a través deladd
método, usando los dos métodos:Método 1: inicialización de doble llave
Método 2: crear una instancia de
ArrayList
yadd
Creé un programa simple para escribir un archivo fuente Java para realizar 1000 inicializaciones usando los dos métodos:
Prueba 1:
Prueba 2:
Tenga en cuenta que el tiempo transcurrido para inicializar la
ArrayList
extensión de 1000 sy 1000 clases anónimas internasArrayList
se 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:
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
ArrayList
tiempo 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
.class
generaron 1000 archivos al compilar elTest1
programa de prueba de inicialización de doble paréntesis.fuente
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).
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.Para ser claro sobre la subclase anónima que se está creando, también puede definir métodos allí. Por ejemplo, anular
HashSet.add()
fuente
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:
... producirá estas clases:
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:
El devuelto
Map
contendrá ahora una referencia a la instancia adjunta deReallyHeavyObject
. Probablemente no quiera arriesgarse a que: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:
Algunas personas pueden encontrar esto sintácticamente estimulante.
fuente
Tomando la siguiente clase de prueba:
y luego descompilando el archivo de clase, veo:
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):
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.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: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:
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
flavors
no irá en contraHashSet
sino en contraMyHashSet
. 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 .
fuente
super()
como segunda línea del constructor implícito, pero tiene que venir primero. (Lo he probado y no se compilará.)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.
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 comonew LinkedHashSet(Arrays.asList("xxx","YYY"));
fuente
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.
huellas dactilares
fuente
compareCollections
combinarse 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.Para crear conjuntos, puede usar un método de fábrica de varargs en lugar de la inicialización de doble llave:
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.
fuente
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í:La limitación de este enfoque es, por supuesto, que no puede controlar el tipo específico de lista que se generará.
fuente
new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))
para solucionar este problema.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.
fuente
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:
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 .
fuente
static
contextos). El enlace propenso a errores en la parte inferior de mi pregunta discute esto más a fondo.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
Déjame saber lo que piensas :)
fuente
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):
fuente
HashSet
capacidad sugerida: recuerde el factor de carga).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).
fuente
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:
y lo usa así:
Google Collections, ahora parte de Guava, respalda una idea similar para la construcción de listas. En esta entrevista , Jared Levy dice:
10/07/2014: Si tan solo pudiera ser tan simple como el de Python:
21/02/2020: en Java 11 ahora puedes decir:
fuente
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.Me parece que el contexto es el objeto devuelto por
new
, que es elHashSet
.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.
fuente