Dans Java 8, pourquoi la capacité par défaut de ArrayList est-elle désormais nulle?

93

Si je me souviens bien, avant Java 8, la capacité par défaut ArrayListétait de 10.

Étonnamment, le commentaire sur le constructeur par défaut (void) dit toujours: 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
la source

Réponses:

105

Techniquement, ce n'est 10pas zéro si vous admettez une initialisation paresseuse du tableau de sauvegarde. Voir:

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

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

Ce à quoi vous faites référence est simplement l'objet de tableau initial de taille zéro qui est partagé entre tous les ArrayListobjets initialement vides . C'est-à-dire que la capacité de 10est garantie paresseusement , une optimisation qui est également présente dans Java 7.

Certes, le contrat constructeur n'est pas entièrement exact. C'est peut-être là une source de confusion.

Contexte

Voici un e-mail de Mike Duigou

J'ai publié une version mise à jour du patch vide ArrayList et HashMap.

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

Cette implémentation révisée n'introduit aucun nouveau champ dans aucune des classes. Pour ArrayList, l'allocation différée du tableau de sauvegarde se produit uniquement si la liste est créée à la taille par défaut. Selon notre équipe d'analyse des performances, environ 85% des instances ArrayList sont créées à la taille par défaut, donc cette optimisation sera valide pour une majorité écrasante de cas.

Pour HashMap, la création utilise le champ de seuil pour suivre la taille initiale demandée jusqu'à ce que le tableau de bucket soit nécessaire. Du côté lecture, le cas de la carte vide est testé avec isEmpty (). Sur la taille d'écriture, une comparaison de (table == EMPTY_TABLE) est utilisée pour détecter la nécessité de gonfler le tableau de compartiment. Dans readObject, il y a un peu plus de travail pour essayer de choisir une capacité initiale efficace.

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

Lukas Eder
la source
4
Selon bugs.java.com/bugdatabase/view_bug.do?bug_id=7143928, cela conduit à réduire l'utilisation du tas et à améliorer les temps de réponse (les chiffres pour deux applications sont affichés)
Thomas Kläger
3
@khelwood: ArrayList ne "rapporte" pas vraiment sa capacité, autrement que via ce Javadoc: il n'y a pas de getCapacity()méthode, ou quelque chose comme ça. (Cela dit, quelque chose comme ensureCapacity(7)un no-op pour une ArrayList initialisée par défaut, donc je suppose que nous sommes vraiment censés agir comme si sa capacité initiale était vraiment de 10
....
10
Belle fouille. La capacité initiale par défaut n'est en effet pas zéro, mais 10, le cas par défaut étant alloué paresseusement comme cas particulier. Vous pouvez observer cela si vous ajoutez à plusieurs reprises des éléments à une ArrayListcréation avec le constructeur no-arg vs en passant zéro au intconstructeur, et si vous examinez la taille du tableau interne de manière réfléchie ou dans un débogueur. Dans le cas par défaut, le tableau passe de la longueur 0 à 10, puis à 15, 22, suivant le taux de croissance de 1,5x. Passer à zéro comme capacité initiale entraîne une croissance de 0 à 1, 2, 3, 4, 6, 9, 13, 19 ....
Stuart marque
13
Je suis Mike Duigou, auteur du changement et du courriel cité et j'approuve ce message. 🙂 Comme le dit Stuart, la motivation était principalement d'économiser de l'espace plutôt que de performances, bien qu'il y ait également un léger avantage en termes de performances en évitant fréquemment la création de la matrice de support.
Mike Duigou
4
@assylias:; ^) non, il a toujours sa place car un singleton emptyList()consomme toujours moins de mémoire que plusieurs ArrayListinstances vides . C'est juste moins important maintenant et donc pas nécessaire à chaque endroit, surtout pas aux endroits avec une probabilité plus élevée d'ajouter des éléments plus tard. Gardez également à l'esprit que vous voulez parfois une liste vide immuable et que emptyList()c'est la voie à suivre.
Holger
24

Dans java 8, la capacité par défaut d'ArrayList est de 0 jusqu'à ce que nous ajoutions au moins un objet dans l'objet ArrayList (vous pouvez l'appeler initialisation paresseuse).

Maintenant, la question est de savoir pourquoi ce changement a été effectué dans JAVA 8?

La réponse est d'économiser la consommation de mémoire. Des millions d'objets de liste de tableaux sont créés dans des applications Java en temps réel. La taille par défaut de 10 objets signifie que nous allouons 10 pointeurs (40 ou 80 octets) pour le tableau sous-jacent à la création et les remplissons avec des valeurs nulles. Un tableau vide (rempli de valeurs nulles) occupe beaucoup de mémoire.

L'initialisation différée reporte cette consommation de mémoire jusqu'au moment où vous utiliserez réellement la liste des tableaux.

Veuillez consulter le code ci-dessous pour obtenir de l'aide.

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

Article La capacité par défaut de ArrayList dans Java 8 l' explique en détail.

Shailesh Vikram Singh
la source
7

Si la toute première opération effectuée avec un ArrayList consiste à transmettre addAllune collection qui a plus de dix éléments, alors tout effort mis dans la création d'un tableau initial de dix éléments pour contenir le contenu de ArrayList serait rejeté par la fenêtre. Chaque fois que quelque chose est ajouté à une ArrayList, il est nécessaire de tester si la taille de la liste résultante dépassera la taille du magasin de sauvegarde; autoriser le magasin de stockage initial à avoir une taille de zéro au lieu de dix entraînera l'échec de ce test une fois de plus dans la durée de vie d'une liste dont la première opération est un "ajout" qui nécessiterait la création du tableau initial de dix éléments, mais ce coût est moins que le coût de création d'un tableau de dix éléments qui ne finit jamais par être utilisé.

Cela dit, il aurait pu être possible d'améliorer encore les performances dans certains contextes s'il y avait une surcharge de "addAll" qui spécifiait combien d'éléments (le cas échéant) seraient probablement ajoutés à la liste après la liste actuelle, et lesquels pourraient utilisez cela pour influencer son comportement d'allocation. Dans certains cas, le code qui ajoute les derniers éléments à une liste aura une assez bonne idée que la liste n'aura jamais besoin d'espace au-delà de cela. Il existe de nombreuses situations où une liste sera remplie une fois et ne sera jamais modifiée par la suite. Si au point code sait que la taille ultime d'une liste sera de 170 éléments, elle comporte 150 éléments et un magasin de sauvegarde de taille 160,

supercat
la source
Très bons points à propos addAll(). C'est encore une autre opportunité d'améliorer l'efficacité autour du premier malloc.
kevinarpe
@kevinarpe: J'aurais aimé que la bibliothèque de Java ait conçu d'autres manières pour que les programmes indiquent comment les choses étaient susceptibles d'être utilisées. L'ancien style de sous-chaîne, par exemple, était moche pour certains cas d'utilisation, mais excellent pour d'autres. S'il y avait eu des fonctions distinctes pour «sous-chaîne qui est susceptible de durer plus longtemps que l'original» et «sous-chaîne qui est peu susceptible de durer plus longtemps que l'original», et si le code a utilisé la bonne 90% du temps, je pense que celles-ci auraient pu largement surperformer soit le ancienne ou nouvelle implémentation de chaîne.
supercat du
3

La question est «pourquoi?».

Inspections de profilage de la mémoire (par exemple ( https://www.yourkit.com/docs/java/help/inspections_mem.jsp#sparse_arrays ) montrent que les tableaux vides (remplis de valeurs nulles) occupent des tonnes de mémoire.

La taille par défaut de 10 objets signifie que nous allouons 10 pointeurs (40 ou 80 octets) pour le tableau sous-jacent à la création et les remplissons avec des valeurs nulles. Les vraies applications Java créent des millions de listes de baies.

La modification introduite supprime ^ W reporter cette consommation de mémoire jusqu'au moment où vous utiliserez réellement la liste des tableaux.

ya_pulser
la source
Veuillez corriger «consommer» par «déchets». Le lien que vous fournissez n'implique pas qu'ils commencent à engloutir de la mémoire partout, mais simplement que les tableaux avec des éléments nuls gaspillent la mémoire qui leur est allouée, de manière disproportionnée. «Consommer» implique qu'ils utilisent comme par magie la mémoire au-delà de leur allocation, ce qui n'est pas le cas.
mechalynx
1

Après la question ci-dessus, j'ai parcouru le document ArrayList de Java 8. J'ai trouvé que la taille par défaut est toujours de 10 seulement.

Veuillez voir ci-dessous

Rahul Maurya
la source
0

La taille par défaut de ArrayList dans JAVA 8 est stil 10. La seule modification apportée à JAVA 8 est que si un codeur ajoute des éléments inférieurs à 10, les espaces vides de la liste de tableaux restants ne sont pas spécifiés à null. Le dire parce que j'ai moi-même traversé cette situation et que l'éclipse m'a fait examiner ce changement de JAVA 8.

Vous pouvez justifier ce changement en regardant la capture d'écran ci-dessous. Dans celui-ci, vous pouvez voir que la taille ArrayList est spécifiée comme 10 dans Object [10] mais le nombre d'éléments affichés n'est que de 7. Les éléments restants à valeur nulle ne sont pas affichés ici. Dans JAVA 7, la capture d'écran ci-dessous est la même avec un seul changement, à savoir que les éléments de valeur nulle sont également affichés pour lesquels le codeur doit écrire du code pour gérer les valeurs nulles s'il itère la liste complète des tableaux tandis que dans JAVA 8, cette charge est supprimée le chef du codeur / développeur.

Lien de capture d'écran.

TechTeddy
la source