En Java 8, ¿por qué la capacidad predeterminada de ArrayList ahora es cero?

93

Según recuerdo, antes de Java 8, la capacidad predeterminada ArrayListera 10.

Sorprendentemente, el comentario sobre el constructor predeterminado (vacío) todavía dice: Constructs an empty list with an initial capacity of ten.

De ArrayList.java:

/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

...

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
kevinarpe
fuente

Respuestas:

105

Técnicamente, no es 10cero, si admite una inicialización perezosa de la matriz de respaldo. Ver:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

dónde

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

A lo que se refiere es solo al objeto de matriz inicial de tamaño cero que se comparte entre todos los ArrayListobjetos inicialmente vacíos . Es decir, la capacidad de 10está garantizada de forma perezosa , una optimización que está presente también en Java 7.

Es cierto que el contrato de constructor no es del todo exacto. Quizás esta sea la fuente de confusión aquí.

Antecedentes

Aquí hay un correo electrónico de Mike Duigou

He publicado una versión actualizada del parche ArrayList y HashMap vacío.

http://cr.openjdk.java.net/~mduigou/JDK-7143928/1/webrev/

Esta implementación revisada no introduce nuevos campos en ninguna de las clases. Para ArrayList, la asignación diferida de la matriz de respaldo ocurre solo si la lista se crea con el tamaño predeterminado. Según nuestro equipo de análisis de rendimiento, aproximadamente el 85% de las instancias de ArrayList se crean con el tamaño predeterminado, por lo que esta optimización será válida para una gran mayoría de casos.

Para HashMap, se hace un uso creativo del campo de umbral para rastrear el tamaño inicial solicitado hasta que se necesita la matriz de cubos. En el lado de lectura, el caso del mapa vacío se prueba con isEmpty (). En el tamaño de escritura, se utiliza una comparación de (tabla == EMPTY_TABLE) para detectar la necesidad de inflar la matriz de cubos. En readObject hay un poco más de trabajo para intentar elegir una capacidad inicial eficiente.

De: http://mail.openjdk.java.net/pipermail/core-libs-dev/2013-April/015585.html

Lukas Eder
fuente
4
Según bugs.java.com/bugdatabase/view_bug.do?bug_id=7143928 conduce a reducir el uso del montón y mejorar los tiempos de respuesta (se muestran los números para dos aplicaciones)
Thomas Kläger
3
@khelwood: ArrayList realmente no "informa" su capacidad, salvo a través de este Javadoc: no hay ningún getCapacity()método, ni nada por el estilo. (Dicho esto, algo así como no ensureCapacity(7)es una operación para una ArrayList inicializada por defecto, así que supongo que realmente se supone que debemos actuar como si su capacidad inicial fuera realmente 10 ...)
ruakh
10
Buena excavación. De hecho, la capacidad inicial predeterminada no es cero, sino 10, y el caso predeterminado se asigna perezosamente como un caso especial. Puede observar esto si agrega repetidamente elementos a un ArrayListcreado con el constructor no-arg en lugar de pasar cero al intconstructor, y si observa el tamaño de la matriz interna de forma reflexiva o en un depurador. En el caso predeterminado, la matriz salta de la longitud 0 a 10, luego a 15, 22, siguiendo la tasa de crecimiento de 1.5x. Pasar cero como capacidad inicial da como resultado un crecimiento de 0 a 1, 2, 3, 4, 6, 9, 13, 19 ...
Stuart Marks
13
Soy Mike Duigou, autor del cambio y del correo electrónico citado y apruebo este mensaje. 🙂 Como dice Stuart, la motivación se centró principalmente en el ahorro de espacio más que en el rendimiento, aunque también hay un ligero beneficio de rendimiento debido a que se evita con frecuencia la creación de la matriz de respaldo.
Mike Duigou
4
@assylias:; ^) no, todavía tiene su lugar ya que un singleton emptyList()aún consume menos memoria que varias ArrayListinstancias vacías . Es menos importante ahora y, por lo tanto, no es necesario en todos los lugares, especialmente en lugares con una mayor probabilidad de agregar elementos en un momento posterior. También tenga en cuenta que a veces desea una lista vacía inmutable y luego emptyList()es el camino a seguir.
Holger
24

En java 8, la capacidad predeterminada de ArrayList es 0 hasta que agreguemos al menos un objeto en el objeto ArrayList (puede llamarlo inicialización diferida).

Ahora la pregunta es ¿por qué se ha realizado este cambio en JAVA 8?

La respuesta es ahorrar consumo de memoria. Se crean millones de objetos de lista de matrices en aplicaciones Java en tiempo real. El tamaño predeterminado de 10 objetos significa que asignamos 10 punteros (40 u 80 bytes) para la matriz subyacente en la creación y los completamos con nulos. Una matriz vacía (llena de nulos) ocupa mucha memoria.

La inicialización perezosa pospone este consumo de memoria hasta el momento en que realmente utilizará la lista de matrices.

Consulte el código a continuación para obtener ayuda.

ArrayList al = new ArrayList();          //Size:  0, Capacity:  0
ArrayList al = new ArrayList(5);         //Size:  0, Capacity:  5
ArrayList al = new ArrayList(new ArrayList(5)); //Size:  0, Capacity:  0
al.add( "shailesh" );                    //Size:  1, Capacity: 10

public static void main( String[] args )
        throws Exception
    {
        ArrayList al = new ArrayList();
        getCapacity( al );
        al.add( "shailesh" );
        getCapacity( al );
    }

    static void getCapacity( ArrayList<?> l )
        throws Exception
    {
        Field dataField = ArrayList.class.getDeclaredField( "elementData" );
        dataField.setAccessible( true );
        System.out.format( "Size: %2d, Capacity: %2d%n", l.size(), ( (Object[]) dataField.get( l ) ).length );
}

Response: - 
Size:  0, Capacity:  0
Size:  1, Capacity: 10

Artículo La capacidad predeterminada de ArrayList en Java 8 lo explica en detalle.

Shailesh Vikram Singh
fuente
7

Si la primera operación que se realiza con una ArrayList es pasar addAlluna colección que tiene más de diez elementos, entonces cualquier esfuerzo realizado para crear una matriz inicial de diez elementos para contener el contenido de ArrayList se eliminaría por la ventana. Siempre que se agrega algo a una ArrayList, es necesario probar si el tamaño de la lista resultante excederá el tamaño del almacén de respaldo; Permitir que la tienda de respaldo inicial tenga un tamaño de cero en lugar de diez hará que esta prueba falle una vez más en la vida útil de una lista cuya primera operación es una "adición" que requeriría crear la matriz inicial de diez elementos, pero ese costo es menos que el costo de crear una matriz de diez elementos que nunca termina usándose.

Dicho esto, podría haber sido posible mejorar aún más el rendimiento en algunos contextos si hubiera una sobrecarga de "addAll" que especificara cuántos elementos (si los hubiera) probablemente se agregarían a la lista después de la actual, y cuáles podrían utilícelo para influir en su comportamiento de asignación. En algunos casos, el código que agrega los últimos elementos a una lista tendrá una idea bastante clara de que la lista nunca necesitará más espacio. Hay muchas situaciones en las que una lista se completa una vez y nunca se modifica después de eso. Si en el punto el código sabe que el tamaño final de una lista será de 170 elementos, tiene 150 elementos y un almacén de respaldo de tamaño 160,

Super gato
fuente
Muy buenos puntos sobre addAll(). Esa es otra oportunidad más para mejorar la eficiencia alrededor del primer malloc.
kevinarpe
@kevinarpe: Desearía que la biblioteca de Java se hubiera diseñado de alguna manera más para que los programas indiquen cómo es probable que se usen las cosas. El antiguo estilo de subcadena, por ejemplo, era pésimo para algunos casos de uso, pero excelente para otros. Si hubiera funciones separadas para "subcadena que probablemente durará más que el original" y "subcadena que es poco probable que dure más que el original", y el código hubiera utilizado la correcta el 90% del tiempo, creo que podrían haber superado en gran medida a la implementación de cadena nueva o antigua.
supercat
3

La pregunta es '¿por qué?'.

Las inspecciones de perfiles de memoria (por ejemplo ( https://www.yourkit.com/docs/java/help/inspections_mem.jsp#sparse_arrays ) muestran que las matrices vacías (llenas de nulos) ocupan toneladas de memoria.

El tamaño predeterminado de 10 objetos significa que asignamos 10 punteros (40 u 80 bytes) para la matriz subyacente en la creación y los completamos con nulos. Las aplicaciones Java reales crean millones de listas de matrices.

La modificación introducida elimina ^ W pospone este consumo de memoria hasta el momento en que realmente utilizará la lista de matrices.

ya_pulser
fuente
Corrija "consumir" con "desperdicio". El enlace que proporciona no implica que comiencen a engullir memoria en todas partes, solo que las matrices con elementos nulos desperdician la memoria que se les asigna, de manera desproporcionada. "Consumir" implica que mágicamente usan la memoria más allá de su asignación, lo que no es el caso.
mechalynx
1

Después de la pregunta anterior, revisé el documento ArrayList de Java 8. Encontré que el tamaño predeterminado sigue siendo solo 10.

Por favor ver más abajo

Rahul Maurya
fuente
0

El tamaño predeterminado de ArrayList en JAVA 8 sigue siendo 10. El único cambio realizado en JAVA 8 es que si un codificador agrega elementos menores de 10, los lugares en blanco de la lista de arreglos restantes no se especifican como nulos. Decirlo porque yo mismo he pasado por esta situación y el eclipse me hizo investigar este cambio de JAVA 8.

Puede justificar este cambio mirando la siguiente captura de pantalla. En él puede ver que el tamaño de ArrayList se especifica como 10 en Object [10], pero el número de elementos mostrados es sólo 7. Los elementos de valor nulo en reposo no se muestran aquí. En JAVA 7, la captura de pantalla a continuación es la misma con un solo cambio, que es que los elementos de valor nulo también se muestran para los cuales el codificador necesita escribir código para manejar valores nulos si está iterando la lista completa de matrices mientras que en JAVA 8 esta carga se elimina de el jefe del codificador / desarrollador.

Enlace de captura de pantalla.

TechTeddy
fuente