Sobrecargas de varargs de Java 9, Set.of() y Map.of()
collections java-9 (3)
Esta pregunta ya tiene una respuesta aquí:
Estoy estudiando los métodos de fábrica para colecciones Immutable
. Veo que el método Set.of()
tiene una sobrecarga de 10 varargs (igual para Map.of()
). Realmente no puedo entender por qué hay tantos. Al final, la función ImmutableCollections.SetN<>(elements)
se llama de todos modos.
En la documentación encontré esto:
Si bien esto introduce un poco de desorden en la API, evita la asignación de matrices, la inicialización y la sobrecarga de recolección de basura en la que incurren las llamadas de varargs.
¿Es el desorden realmente vale la ganancia de rendimiento? En caso afirmativo, ¿sería ideal crear un método separado para cualquier elemento N
?
Algunos aspectos de esto pueden ser una forma de pruebas futuras.
Si evoluciona una API, debe prestar atención a cómo cambiará la firma del método, por lo que si tenemos
public class API {
public static final <T> Set<T> of(T... elements) { ... }
}
Podríamos decir que los varargs son lo suficientemente buenos ... excepto que los varargs obligan a la asignación de una matriz de objetos, que, aunque es razonablemente barato, realmente afecta el rendimiento. Consulte, por ejemplo, esta marca microbiológica que muestra una pérdida del 50% del rendimiento para un registro sin operación (es decir, el nivel de registro es más bajo que el loggable) cuando se cambia a la forma de varargs.
Ok, entonces hacemos un análisis y decimos que el caso más común es el singleton, así que decidimos refactorizar ...
public class API {
public static final <T> Set<T> of(T first) { ... }
public static final <T> Set<T> of(T first, T... others) { ... }
}
Ooops ... eso no es compatible con binarios ... es compatible con fuente, pero no compatible con binarios ... para conservar la compatibilidad binaria necesitamos mantener la firma anterior, por ejemplo
public class API {
public static final <T> Set<T> of(T first) { ... }
@Deprecated public static final <T> Set<T> of(T... elements) { ... }
public static final <T> Set<T> of(T first, T... others) { ... }
}
Ugh ... el código IDE completo es un desastre ahora ... además, ¿cómo creo un conjunto de arreglos? (probablemente más relevante si estaba usando una lista) API.of(new Object[0])
es ambiguo ... si solo no hubiéramos agregado el vararg al comienzo ...
Así que creo que lo que hicieron fue agregar suficientes argumentos explícitos para llegar al punto en que el tamaño de pila adicional cubra el costo de la creación de vararg, que es probablemente alrededor de 10 argumentos (al menos en base a las medidas que Log4J2 hizo al agregar sus varargs a la versión 2 API) ... pero lo está haciendo para pruebas basadas en pruebas de futuro ...
En otras palabras, podemos hacer trampa en todos los casos en los que no tenemos evidencia que requiera una implementación especializada y solo se adapte a la variante vararg:
public class API {
private static final <T> Set<T> internalOf(T... elements) { ... }
public static final <T> Set<T> of(T first) { return internalOf(first); }
public static final <T> Set<T> of(T first, T second) { return internalOf(first, second); }
...
public static final <T> Set<T> of(T t1, T t2, T t3, T t4, T t5, T... rest) { ... }
}
Luego podemos perfilar y observar los patrones de uso del mundo real y si vemos un uso significativo hasta la forma de 4 arg y los puntos de referencia que muestran que hay una ganancia de rendimiento razonable, entonces en ese punto, detrás de escena, cambiamos el método impl y todos ganan ... no se requiere una recompilación
Supongo que depende del alcance de la API con la que está trabajando. Cuando se habla de esas clases inmutables, se habla de cosas incluidas como parte de jdk; Así que el alcance es muy amplio.
Así que tienes:
- por un lado, estas clases inmutables pueden ser utilizadas por aplicaciones donde cada bit cuenta (y cada nanosegundo se desperdicia en asignación / desasignación).
- en el otro lado, que las aplicaciones sin esas necesidades no se vean afectadas negativamente
- el único lado ''negativo'' es para los implementadores de esa API que tendrán que lidiar más con el desorden, por lo que afecta la capacidad de mantenimiento (pero no es una gran cosa en este caso).
Si estuvieras implementando tus propias cosas, no me importaría mucho eso (pero ten cuidado con los argumentos de varargs) a menos que realmente necesites preocuparte por esos bits adicionales (y el rendimiento adicional, etc.).
En el momento en que se llama ese método de todos modos, esto podría cambiar. Por ejemplo, podría ser que crea un Set
con solo tres elementos, 4 y así sucesivamente.
Además, no todos ellos delegan en SetN
, los que tienen cero, uno y dos elementos tienen clases reales de ImmutableCollections.Set0
, ImmutableCollections.Set1
y ImmutableCollections.Set2
O puede leer la pregunta real sobre esto ... aquí Lea los comentarios de Stuart Marks
en esa pregunta, ya que él es la persona que creó estas Colecciones.