Je lisais le code source ArrayList
de Java et j'ai remarqué certaines comparaisons dans les déclarations if.
En Java 7, la méthode grow(int)
uses
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
En Java 6, grow
n'existait pas. La méthode ensureCapacity(int)
utilise cependant
if (newCapacity < minCapacity)
newCapacity = minCapacity;
Quelle était la raison derrière le changement? Était-ce un problème de performance ou juste un style?
J'imagine que comparer avec zéro est plus rapide, mais effectuer une soustraction complète pour vérifier si elle est négative me semble un peu exagéré. Toujours en termes de code binaire, cela impliquerait deux instructions (ISUB
et IF_ICMPGE
) au lieu d'une (IFGE
).
a < b
et a - b < 0
peuvent signifier deux choses différentes. Considérons le code suivant:
int a = Integer.MAX_VALUE;
int b = Integer.MIN_VALUE;
if (a < b) {
System.out.println("a < b");
}
if (a - b < 0) {
System.out.println("a - b < 0");
}
Lorsqu'il est exécuté, ceci imprimera seulement a - b < 0
. Ce qui se passe, c'est que a < b
est clairement faux, mais a - b
déborde et devient -1
, ce qui est négatif.
Cela dit, considérons que la longueur du tableau est très proche de Integer.MAX_VALUE
. Le code dans ArrayList
va comme ceci:
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
oldCapacity
est vraiment proche de Integer.MAX_VALUE
donc newCapacity
(qui est oldCapacity + 0.5 * oldCapacity
) pourrait déborder et devenir Integer.MIN_VALUE
(c’est-à-dire négatif). Ensuite, soustrayez minCapacity
underflow retour en un nombre positif.
Cette vérification garantit que la if
n'est pas exécutée. Si le code était écrit avec if (newCapacity < minCapacity)
, il s'agirait de true
dans ce cas (puisque newCapacity
est négatif), de sorte que newCapacity
serait forcé à minCapacity
sans tenir compte de oldCapacity
.
Ce cas de débordement est traité par le prochain if. Lorsque newCapacity
a débordé, ce sera true
: MAX_ARRAY_SIZE
est défini comme Integer.MAX_VALUE - 8
et Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0
est true
. La variable newCapacity
est donc correctement gérée: la méthode hugeCapacity
renvoie MAX_ARRAY_SIZE
ou Integer.MAX_VALUE
.
NB: c'est ce que dit le commentaire // overflow-conscious code
dans cette méthode.
J'ai trouvé cette explication :
Le mardi 9 mars 2010 à 03h02, Kevin L. Stern a écrit:
J'ai fait une recherche rapide et il apparaît que Java est en effet un complément à deux basé. Néanmoins, permettez-moi de souligner que, de manière générale, ceci Ce type de code m'inquiète car je m'attends vraiment à ce que quelqu'un finisse par le faire viens et fais exactement ce que Dmytro a suggéré; c'est-à-dire que quelqu'un le fera changement:
if (a - b > 0)
à
if (a > b)
et tout le navire va couler. Personnellement, j'aime éviter les obscurités comme faire du débordement d’entier une base essentielle de mon algorithme, sauf si il y a une bonne raison de le faire. Je préférerais en général éviter débordement total et pour rendre le scénario de débordement plus explicite:
if (oldCapacity > RESIZE_OVERFLOW_THRESHOLD) { // Do something } else { // Do something else }
C'est un bon point.
Dans
ArrayList
nous ne pouvons pas faire cela (ou du moins ne pas être compatible), carensureCapacity
est une API publique et accepte effectivement déjà nombres négatifs en tant que demandes de capacité positive qui ne peuvent pas être satisfait.L'API actuelle est utilisée comme ceci:
int newcount = count + len; ensureCapacity(newcount);
Si vous voulez éviter les débordements, vous devrez changer quelque chose moins naturel comme
ensureCapacity(count, len); int newcount = count + len;
Quoi qu'il en soit, je garde le code trop sensible au débordement, mais j'ajoute plus de commentaires d'avertissement et "esquisse" la création d'un tableau énorme de telle sorte que Le code de
ArrayList
ressemble maintenant à:/** * Increases the capacity of this <tt>ArrayList</tt> instance, if * necessary, to ensure that it can hold at least the number of elements * specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ public void ensureCapacity(int minCapacity) { modCount++; // Overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } /** * The maximum size of array to allocate. * Some VMs reserve some header words in an array. * Attempts to allocate larger arrays may result in * OutOfMemoryError: Requested array size exceeds VM limit */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ private void grow(int minCapacity) { // Overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } private int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
Webrev régénéré.
Martin
En Java 6, si vous utilisez l'API en tant que:
int newcount = count + len;
ensureCapacity(newcount);
Et newCount
déborde (cela devient négatif), if (minCapacity > oldCapacity)
retournera false et vous pouvez supposer à tort que la ArrayList
a été augmentée de len
.
En regardant le code:
int newCapacity = oldCapacity + (oldCapacity >> 1);
Si oldCapacity
est assez grand, cela débordera et newCapacity
sera un nombre négatif. Une comparaison comme newCapacity < oldCapacity
n'évaluera pas correctement true
et la variable ArrayList
ne pourra pas croître.
Au lieu de cela, le code tel qu'il est écrit (newCapacity - minCapacity < 0
renvoie false) permettra d'évaluer davantage la valeur négative de newCapacity
à la ligne suivante, ce qui entraînera le recalcul de newCapacity
en appelant hugeCapacity
(newCapacity = hugeCapacity(minCapacity);
) pour permettre à ArrayList
de grandir à MAX_ARRAY_SIZE
.
C’est ce que le commentaire // overflow-conscious code
tente de communiquer, bien que de manière oblique.
Ainsi, au final, la nouvelle comparaison protège contre l’allocation d’une ArrayList
plus grande que le MAX_ARRAY_SIZE
prédéfini, tout en lui permettant de croître jusqu’à cette limite si nécessaire.
Les deux formes se comportent exactement de la même façon, sauf si l'expression a - b
déborde, auquel cas elles sont opposées. Si a
est un grand négatif et b
est un grand positif, alors (a < b)
est clairement vrai, mais a - b
débordera pour devenir positif, donc (a - b < 0)
est faux.
Si vous connaissez le code d'assemblage x86, considérez que (a < b)
est implémenté par une jge
, qui se branche autour du corps de l'instruction if lorsque SF = OF. D'autre part, (a - b < 0)
agira comme une jns
, qui se branche lorsque SF = 0. Par conséquent, ils se comportent différemment, précisément lorsque OF = 1.