referencia - para que sirve el comando super en java
¿Horrible rendimiento y gran espacio de almacenamiento dinámico de referencia de constructor Java 8? (2)
Acabo de tener una experiencia bastante desagradable en nuestro entorno de producción, causando
OutOfMemoryErrors: heapspace..
Rastreé el problema con mi uso de
ArrayList::new
en una función.
Para verificar que esto realmente está funcionando peor que la creación normal a través de un constructor declarado (
t -> new ArrayList<>()
), escribí el siguiente método pequeño:
public class TestMain {
public static void main(String[] args) {
boolean newMethod = false;
Map<Integer,List<Integer>> map = new HashMap<>();
int index = 0;
while(true){
if (newMethod) {
map.computeIfAbsent(index, ArrayList::new).add(index);
} else {
map.computeIfAbsent(index, i->new ArrayList<>()).add(index);
}
if (index++ % 100 == 0) {
System.out.println("Reached index "+index);
}
}
}
}
Ejecutando el método con
newMethod=true;
hará que el método falle con
OutOfMemoryError
justo después de que el índice
OutOfMemoryError
a 30k.
Con
newMethod=false;
el programa no falla, pero sigue golpeando hasta que se mata (el índice alcanza fácilmente 1.5 millones).
¿Por qué
ArrayList::new
crea tantos elementos
Object[]
en el montón que causa
OutOfMemoryError
tan rápido?
(Por cierto, también ocurre cuando el tipo de colección es
HashSet
).
En el primer caso (
ArrayList::new
) está utilizando el
constructor
que toma un argumento de capacidad inicial, en el segundo caso no lo está.
Una gran capacidad inicial (
index
en su código) hace que se asigne un gran
Object[]
, lo que da como resultado su
OutOfMemoryError
s.
Aquí están las implementaciones actuales de los dos constructores:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
Algo similar sucede en
HashSet
, excepto que la matriz no se asigna hasta que se llama a
add
.
La firma
computeIfAbsent
es la siguiente:
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
Entonces el
mappingFunction
es la función que
recibe
un argumento.
En su caso,
K = Integer
y
V = List<Integer>
, por lo que la firma se convierte (omitiendo PECS):
Function<Integer, List<Integer>> mappingFunction
Cuando escribe
ArrayList::new
en el lugar donde es necesaria la
Function<Integer, List<Integer>>
, el compilador busca el constructor adecuado que es:
public ArrayList(int initialCapacity)
Así que esencialmente su código es equivalente a
map.computeIfAbsent(index, i->new ArrayList<>(i)).add(index);
Y sus claves se tratan como valores de
initialCapacity
que conducen a la preasignación de matrices de tamaño cada vez mayor, lo que, por supuesto, bastante rápido conduce a
OutOfMemoryError
.
En este caso particular, las referencias de constructor no son adecuadas.
Use lambdas en su lugar.
¿Fue el
Supplier<? extends V>
Supplier<? extends V>
usado en
computeIfAbsent
, entonces
ArrayList::new
sería apropiado.