In Java 8, perché la capacità predefinita di ArrayList ora è zero?


93

Come ricordo, prima di Java 8, la capacità predefinita di ArrayListera 10.

Sorprendentemente, il commento sul costruttore predefinito (void) dice ancora: Constructs an empty list with an initial capacity of ten.

Da 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;
}

Risposte:


105

Tecnicamente, è 10, non zero, se ammetti un'inizializzazione pigra dell'array di supporto. Vedere:

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

dove

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

Quello a cui ti riferisci è solo l'oggetto della matrice iniziale di dimensione zero che è condiviso tra tutti gli ArrayListoggetti inizialmente vuoti . Cioè la capacità di 10è garantita pigramente , un'ottimizzazione che è presente anche in Java 7.

Certo, il contratto del costruttore non è del tutto accurato. Forse questa è la fonte di confusione qui.

sfondo

Ecco un'e-mail di Mike Duigou

Ho pubblicato una versione aggiornata della patch vuota di ArrayList e HashMap.

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

Questa implementazione rivista non introduce nuovi campi in nessuna delle classi. Per ArrayList l'allocazione lenta dell'array di supporto si verifica solo se l'elenco viene creato con le dimensioni predefinite. Secondo il nostro team di analisi delle prestazioni, circa l'85% delle istanze di ArrayList viene creato con le dimensioni predefinite, quindi questa ottimizzazione sarà valida per la stragrande maggioranza dei casi.

Per HashMap, viene utilizzato in modo creativo il campo soglia per tenere traccia della dimensione iniziale richiesta fino a quando non è necessario l'array di bucket. Sul lato di lettura, il caso della mappa vuota viene testato con isEmpty (). Sulla dimensione di scrittura viene utilizzato un confronto di (table == EMPTY_TABLE) per rilevare la necessità di gonfiare l'array di bucket. In readObject c'è ancora un po 'di lavoro per cercare di scegliere una capacità iniziale efficiente.

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


4
Secondo bugs.java.com/bugdatabase/view_bug.do?bug_id=7143928 porta a ridurre l'utilizzo dell'heap e migliorare i tempi di risposta (vengono mostrati i numeri per due applicazioni)
Thomas Kläger

3
@khelwood: ArrayList non "riporta" realmente la sua capacità, a parte questo Javadoc: non esiste un getCapacity()metodo o qualcosa del genere. (Detto questo, qualcosa come ensureCapacity(7)è un no-op per un ArrayList inizializzato di default, quindi immagino che dovremmo davvero agire come se la sua capacità iniziale fosse veramente 10.)
ruakh

10
Bello scavare. La capacità iniziale predefinita non è infatti zero, ma 10, con il caso predefinito che viene allocato pigramente come caso speciale. Puoi osservarlo se aggiungi ripetutamente elementi a un ArrayListcreato con il costruttore no-arg anziché passare zero al intcostruttore e se guardi la dimensione dell'array interno in modo riflessivo o in un debugger. Nel caso predefinito l'array salta dalla lunghezza 0 a 10, quindi a 15, 22, seguendo il tasso di crescita di 1,5x. Passare a zero come capacità iniziale si traduce in una crescita da 0 a 1, 2, 3, 4, 6, 9, 13, 19 ...
Stuart Marks

13
Sono Mike Duigou, autore della modifica e dell'email citata e approvo questo messaggio. 🙂 Come afferma Stuart, la motivazione era principalmente sul risparmio di spazio piuttosto che sulle prestazioni, sebbene vi sia anche un leggero vantaggio in termini di prestazioni dovuto al fatto che si evita frequentemente la creazione dell'array di supporto.
Mike Duigou

4
@assylias:; ^) no, ha ancora il suo posto in quanto un singleton emptyList()consuma ancora meno memoria di diverse ArrayLististanze vuote . È solo meno importante ora e quindi non è necessario in ogni luogo, soprattutto non in luoghi con una maggiore probabilità di aggiungere elementi in un secondo momento. Inoltre, tieni presente che a volte vuoi un elenco vuoto immutabile e quindi emptyList()è la strada da percorrere.
Holger

24

In java 8 la capacità predefinita di ArrayList è 0 finché non aggiungiamo almeno un oggetto nell'oggetto ArrayList (puoi chiamarlo inizializzazione pigra).

Ora la domanda è perché questo cambiamento è stato fatto in JAVA 8?

La risposta è risparmiare il consumo di memoria. Milioni di oggetti elenco array vengono creati in applicazioni Java in tempo reale. La dimensione predefinita di 10 oggetti significa che assegniamo 10 puntatori (40 o 80 byte) per l'array sottostante al momento della creazione e li riempiamo con valori nulli. Un array vuoto (riempito con valori nulli) occupa molta memoria.

L'inizializzazione lenta posticipa questo consumo di memoria fino al momento in cui utilizzerai effettivamente l'elenco degli array.

Si prega di consultare il codice sottostante per assistenza.

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

L'articolo Capacità predefinita di ArrayList in Java 8 lo spiega in dettaglio.


7

Se la prima operazione che viene eseguita con un ArrayList deve passare addAll una raccolta che ha più di dieci elementi, qualsiasi sforzo compiuto per creare un array iniziale di dieci elementi per contenere il contenuto di ArrayList verrebbe buttato fuori dalla finestra. Ogni volta che viene aggiunto qualcosa a un ArrayList, è necessario verificare se la dimensione della lista risultante supererà la dimensione del backing store; consentendo al backing store iniziale di avere dimensione zero anziché dieci, questo test fallirà una volta in più nel corso della vita di un elenco la cui prima operazione è un "add" che richiederebbe la creazione dell'array iniziale di dieci elementi, ma il costo è meno del costo di creazione di un array di dieci elementi che non finisce mai per essere utilizzato.

Detto questo, sarebbe stato possibile migliorare ulteriormente le prestazioni in alcuni contesti se ci fosse stato un sovraccarico di "addAll" che specificasse quanti elementi (se presenti) sarebbero stati probabilmente aggiunti all'elenco dopo quello attuale, e quali potrebbero usalo per influenzarne il comportamento di allocazione. In alcuni casi il codice che aggiunge gli ultimi elementi a un elenco avrà un'idea abbastanza chiara che l'elenco non avrà mai bisogno di spazio oltre a quello. Ci sono molte situazioni in cui un elenco viene popolato una volta e non viene mai modificato dopo. Se al punto il codice sa che la dimensione finale di un elenco sarà di 170 elementi, ha 150 elementi e un archivio di supporto di dimensione 160,


Aspetti molto positivi addAll(). Questa è un'altra opportunità per migliorare l'efficienza attorno al primo malloc.
kevinarpe

@kevinarpe: Vorrei che la libreria di Java fosse stata progettata in altri modi per i programmi per indicare come le cose avrebbero potuto essere utilizzate. Il vecchio stile di sottostringa, ad esempio, era pessimo per alcuni casi d'uso, ma eccellente per altri. Se ci fossero state funzioni separate per "sottostringa che probabilmente sopravviverà all'originale" e "sottostringa che è improbabile che durerà più a lungo dell'originale" e il codice usasse quella giusta il 90% delle volte, penserei che quelle avrebbero potuto superare di gran lunga le prestazioni implementazione di stringhe vecchie o nuove.
supercat

3

La domanda è "perché?".

Le ispezioni del profilo della memoria (ad esempio ( https://www.yourkit.com/docs/java/help/inspections_mem.jsp#sparse_arrays ) mostrano che gli array vuoti (pieni di null) occupano tonnellate di memoria.

La dimensione predefinita di 10 oggetti significa che assegniamo 10 puntatori (40 o 80 byte) per l'array sottostante al momento della creazione e li riempiamo con valori nulli. Le vere applicazioni Java creano milioni di elenchi di array.

La modifica introdotta rimuove ^ W posticipare questo consumo di memoria fino al momento in cui utilizzerai effettivamente l'elenco degli array.


Correggi "consuma" con "rifiuti". Il collegamento fornito non implica che inizino a divorare memoria ovunque, ma solo che gli array con elementi nulli sprecano la memoria che è stata loro assegnata, in modo sproporzionato. "Consumo" implica che usano magicamente la memoria oltre la loro allocazione, il che non è il caso.
mechalynx

1

Dopo la domanda precedente ho esaminato il documento ArrayList di Java 8. Ho scoperto che la dimensione predefinita è ancora solo 10.

Vedi sotto


0

La dimensione predefinita di ArrayList in JAVA 8 è stil 10. L'unica modifica apportata in JAVA 8 è che se un programmatore aggiunge elementi inferiori a 10, i restanti spazi vuoti dell'elenco di array non vengono specificati su null. Detto così perché io stesso ho attraversato questa situazione e Eclipse mi ha fatto esaminare questo cambiamento di JAVA 8.

Puoi giustificare questo cambiamento guardando lo screenshot qui sotto. In esso puoi vedere che la dimensione di ArrayList è specificata come 10 in Object [10] ma il numero di elementi visualizzati è solo 7. Gli elementi con valore nullo di resto non vengono visualizzati qui. In JAVA 7 lo screenshot qui sotto è lo stesso con una sola modifica, ovvero che vengono visualizzati anche gli elementi con valore nullo per i quali il programmatore deve scrivere il codice per la gestione dei valori nulli se sta iterando l'elenco completo di array mentre in JAVA 8 questo onere viene rimosso da il capo del programmatore / sviluppatore.

Collegamento dello screenshot.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.