Lors de l'utilisation d'ArrayList en Java, une modification de la collection pendant l'itération peut déclencher une ConcurrentModificationException. Cela se produit généralement lorsque l'on supprime des éléments avec la méthode remove sans utiliser l'itérateur approprié.
Considérons un exemple de code pour illustrer le problème :
List<String> elements = new ArrayList<>();
elements.add("alpha");
elements.add("beta");
for (String elem : elements) {
if (elem.equals("alpha")) {
elements.remove(elem);
}
}
Après compilation, la boucle for-each est transformée en utiilsation d'un itérateur :
List<String> elements = new ArrayList<>();
elements.add("alpha");
elements.add("beta");
Iterator<String> iter = elements.iterator();
while (iter.hasNext()) {
String current = iter.next();
if ("alpha".equals(current)) {
elements.remove(current);
}
}
Dans ce scénario, la suppression du premier élément réussit sans erreur, mais la tentative de suppression du deuxième élément entraîne une exception. Pour comprendre pourquoi, il faut examiner les mécanismes internes d'ArrayList.
La classe ArrayList utilise un compteur modCount qui est incrémenté à chaque modification structurelle (ajout ou suppression). Lorsqu'un itérateur est créé, il capture la valeur actuelle de modCount dans un champ expectedModCount. Avant chaque opération sur l'itérateur, une vérification est effectuée pour s'assurer que modCount n'a pas changé ; sinon, une exception est levée.
Voici un extrait de code de la classe interne Itr :
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
Analysons le cas de la suppression du premier élément. Dans l'exemple, la liste contient "alpha" et "beta". Lors de la première itération, cursor est 0 et size est 2, donc hasNext() renvoie vrai. Après suppression de "alpha", size passe à 1, et modCount est incrémenté. Lors de la préparation de la prochaine itération, hasNext() compare cursor (qui est 1) avec la nouvelle size (1), ce qui donne faux, évitant ainsi une entrée dans la boucle et une vérification d'exception.
Pour la suppression du deuxième élément, supposons que la liste ait trois éléments et que l'on supprime le second. Après la première itération, cursor passe à 1, et la suppression modifie modCount et size. Lors de la vérification suivante, modCount a changé par rapport à expectedModCount, ce qui déclenche l'exception. En termes mathématiques, si la taille initiale est n, et que l'on supprime un élément qui n'est pas le dernier, cursor peut atteindre n-1 tandis que size devient n-1, mais la différence dans modCount cause l'erreur.
Pour éviter cette expection, il est recommandé d'utiliser la méthode remove de l'itérateur lui-même, comme ceci :
Iterator<String> iter = elements.iterator();
while (iter.hasNext()) {
String current = iter.next();
if (current.equals("alpha")) {
iter.remove(); // Utilisation de l'itérateur pour une suppression sûre
}
}
Cela garantit que les modifications sont synchronisées avec l'itérateur, maintenant la cohérence entre modCount et expectedModCount.