Supprimer toutes les occurrences d’une valeur spécifique d’une liste

Supprimer toutes les occurrences d'une valeur spécifique d'une liste

1. introduction

En Java, il est simple de supprimer une valeur spécifique d'unList à l'aide deList.remove(). Cependant,efficiently removing all occurrences of a value est beaucoup plus difficile.

Dans ce didacticiel, nous verrons plusieurs solutions à ce problème, décrivant les avantages et les inconvénients.

Pour des raisons de lisibilité, nous utilisons une méthode personnaliséelist(int…) dans les tests, qui renvoie unArrayList contenant les éléments que nous avons passés.

2. Utilisation d'une bouclewhile

Puisque nous savons commentremove a single element, doing it repeatedly in a loop semble assez simple:

void removeAll(List list, int element) {
    while (list.contains(element)) {
        list.remove(element);
    }
}

Cependant, cela ne fonctionne pas comme prévu:

// given
List list = list(1, 2, 3);
int valueToRemove = 1;

// when
assertThatThrownBy(() -> removeAll(list, valueToRemove))
  .isInstanceOf(IndexOutOfBoundsException.class);

Le problème est dans la 3ème ligne: on appelleList.remove(int), which treats its argument as the index, not the value we want to remove.

Dans le test ci-dessus, nous appelons toujourslist.remove(1), mais l'index de l'élément que nous voulons supprimer est0. L'appel deList.remove() décale tous les éléments après celui supprimé vers des indices plus petits.

Dans ce scénario, cela signifie que nous supprimons tous les éléments, sauf le premier.

Lorsqu'il ne reste que le premier, l'index1 sera illégal. Par conséquent, nous obtenons unException.

Notez que nous ne sommes confrontés à ce problème que si nous appelonsList.remove() avec un argument primitifbyte,short, char ouint, puisque la première chose que fait le compilateur lorsqu'il tente de trouver la méthode surchargée correspondante, s'élargit.

Nous pouvons le corriger en passant la valeur enInteger:

void removeAll(List list, Integer element) {
    while (list.contains(element)) {
        list.remove(element);
    }
}

Maintenant, le code fonctionne comme prévu:

// given
List list = list(1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

PuisqueList.contains() etList.remove() doivent tous deux trouver la première occurrence de l'élément, ce code provoque une traversée inutile de l'élément.

Nous pouvons faire mieux si nous stockons l'index de la première occurrence:

void removeAll(List list, Integer element) {
    int index;
    while ((index = list.indexOf(element)) >= 0) {
        list.remove(index);
    }
}

Nous pouvons vérifier que cela fonctionne:

// given
List list = list(1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Bien que ces solutions produisent du code court et propre,they still have poor performance: parce que nous ne suivons pas la progression,List.remove() doit trouver la première occurrence de la valeur fournie pour la supprimer.

De plus, lorsque nous utilisons unArrayList, le décalage d'élément peut provoquer de nombreuses copies de références, voire réallouer plusieurs fois le tableau de sauvegarde.

3. Suppression jusqu'à ce que lesList changent

List.remove(E element) a une fonctionnalité que nous n'avons pas encore mentionnée: ilreturns a boolean value, which is true if the List changed because of the operation, therefore it contained the element.

Notez queList.remove(int index) renvoie void, car si l'index fourni est valide,List le supprime toujours. Sinon, il lanceIndexOutOfBoundsException.

Avec cela, nous pouvons effectuer des suppressions jusqu'à ce que leList change:

void removeAll(List list, int element) {
    while (list.remove(element));
}

Cela fonctionne comme prévu:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Bien qu’elle soit courte, cette implémentation souffre des mêmes problèmes que ceux décrits dans la section précédente.

3. Utilisation d'une bouclefor

Nous pouvons suivre notre progression en parcourant les éléments avec une bouclefor et en supprimant celle actuelle si elle correspond:

void removeAll(List list, int element) {
    for (int i = 0; i < list.size(); i++) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
        }
    }
}

Cela fonctionne comme prévu:

// given
List list = list(1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Cependant, si nous essayons avec une entrée différente, cela donnera une sortie incorrecte:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(1, 2, 3));

Analysons le fonctionnement du code, étape par étape:

  • i = 0

    • element etlist.get(i) sont tous deux égaux à1 à la ligne 3, donc Java entre dans le corps de l'instructionif,

    • on supprime l'élément à l'index0,

    • donclist contient maintenant1,2 et3

  • i = 1

    • list.get(i) renvoie2 carwhen we remove an element from a List, it shifts all proceeding elements to smaller indices

Nous sommes donc confrontés à ce problème lorsquewe have two adjacent values, which we want to remove. Pour résoudre ce problème, nous devons conserver la variable de boucle.

En le diminuant quand on enlève l'élément:

void removeAll(List list, int element) {
    for (int i = 0; i < list.size(); i++) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
            i--;
        }
    }
}

L'augmenter uniquement lorsque nous ne supprimons pas l'élément:

void removeAll(List list, int element) {
    for (int i = 0; i < list.size();) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
        } else {
            i++;
        }
    }
}

Notez que dans ce dernier, nous avons supprimé l'instructioni++ à la ligne 2.

Les deux solutions fonctionnent comme prévu:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Cette mise en œuvre semble convenir à première vue. Cependant, il a toujoursserious performance problems:

  • suppression d'un élément d'unArrayList, décale tous les éléments après lui

  • accéder aux éléments par index dans unLinkedList signifie parcourir les éléments un par un jusqu'à trouver l'index

4. Utilisation d'une bouclefor-each

Depuis Java 5, nous pouvons utiliser la bouclefor-each pour parcourir unList. Utilisons-le pour supprimer des éléments:

void removeAll(List list, int element) {
    for (Integer number : list) {
        if (Objects.equals(number, element)) {
            list.remove(number);
        }
    }
}

Notez que nous utilisonsInteger comme type de variable de boucle. Par conséquent, nous n’obtiendrons pas deNullPointerException.

Aussi, de cette façon, nous invoquonsList.remove(E element), qui attend la valeur que nous voulons supprimer, pas l'index.

Aussi propre que cela puisse paraître, malheureusement, cela ne fonctionne pas:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
assertThatThrownBy(() -> removeWithForEachLoop(list, valueToRemove))
  .isInstanceOf(ConcurrentModificationException.class);

La bouclefor-each utiliseIterator pour traverser les éléments. Cependant,when we modify the List, the Iterator gets into an inconsistent state. Hence it throws ConcurrentModificationException.

La leçon est la suivante: nous ne devons pas modifier unList, alors que nous accédons à ses éléments dans une bouclefor-each.

5. Utilisation d'unIterator

Nous pouvons utiliser lesIterator directement pour parcourir et modifier lesList avec:

void removeAll(List list, int element) {
    for (Iterator i = list.iterator(); i.hasNext();) {
        Integer number = i.next();
        if (Objects.equals(number, element)) {
            i.remove();
        }
    }
}

De cette façon,the Iterator can track the state of the List (car il effectue la modification). Par conséquent, le code ci-dessus fonctionne comme prévu:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Puisque chaque classeList peut fournir sa propre implémentation deIterator, nous pouvons supposer en toute sécurité queit implements element traversing and removal the most efficient way possible.

However, using ArrayList still means lots of element shifting (et peut-être la réallocation du tableau). De plus, le code ci-dessus est légèrement plus difficile à lire, car il diffère de la boucle standardfor, que la plupart des développeurs connaissent.

6. La collecte

Jusque-là, nous avons modifié l’objetList d’origine en supprimant les éléments dont nous n’avions pas besoin. Nous pouvons plutôtcreate a new List and collect the items we want to keep:

List removeAll(List list, int element) {
    List remainingElements = new ArrayList<>();
    for (Integer number : list) {
        if (!Objects.equals(number, element)) {
            remainingElements.add(number);
        }
    }
    return remainingElements;
}

Puisque nous fournissons le résultat dans un nouvel objetList, nous devons le renvoyer depuis la méthode. Par conséquent, nous devons utiliser la méthode d'une autre manière:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
List result = removeAll(list, valueToRemove);

// then
assertThat(result).isEqualTo(list(2, 3));

Notez que nous pouvons maintenant utiliser la bouclefor-each puisque nous ne modifions pas lesList que nous parcourons actuellement.

Comme il n'y a pas de suppression, il n'est pas nécessaire de déplacer les éléments. Par conséquent, cette implémentation fonctionne bien lorsque nous utilisons unArrayList.

Cette implémentation se comporte différemment des précédentes:

  • il ne modifie pas l'originalList mais celui dereturns a new

  • the method decides what the returned List‘s implementation is, il peut être différent de l'original

De plus, nous pouvons modifier notre implémentation enget the old behavior; on efface lesList originaux et on y ajoute les éléments collectés:

void removeAll(List list, int element) {
    List remainingElements = new ArrayList<>();
    for (Integer number : list) {
        if (!Objects.equals(number, element)) {
            remainingElements.add(number);
        }
    }

    list.clear();
    list.addAll(remainingElements);
}

Cela fonctionne de la même façon que les précédents:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Puisque nous ne modifions pas continuellement lesList, nous n’avons pas à accéder aux éléments par position ni à les déplacer. De plus, il n’existe que deux réallocations de tableaux possibles: lorsque nous appelonsList.clear() etList.addAll().

7. Utilisation de l'API Stream

Java 8 a introduit les expressions lambda et les API de flux. Avec ces fonctionnalités puissantes, nous pouvons résoudre notre problème avec un code très propre:

List removeAll(List list, int element) {
    return list.stream()
      .filter(e -> !Objects.equals(e, element))
      .collect(Collectors.toList());
}

Cette solutionworks the same way, like when we were collecting the remaining elements.

As a result, it has the same characteristics, et nous devrions l'utiliser pour renvoyer le résultat:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
List result = removeAll(list, valueToRemove);

// then
assertThat(result).isEqualTo(list(2, 3));

Notez que nous pouvons le convertir pour qu’il fonctionne comme les autres solutions avec la même approche que nous avons utilisée avec la mise en œuvre originale de «collecte».

8. Utilisation deremoveIf

Avec les lambdas et les interfaces fonctionnelles, Java 8 a également introduit certaines extensions API. Par exemple, lesList.removeIf() method, which implements what we saw in the last section.

Il attend unPredicate, qui devrait renvoyer l'élémenttrue when we want to remove, contrairement à l'exemple précédent, où nous devions retournertrue lorsque nous voulions conserver l'élément:

void removeAll(List list, int element) {
    list.removeIf(n -> Objects.equals(n, element));
}

Cela fonctionne comme les autres solutions ci-dessus:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Du fait que leList implémente lui-même cette méthode, nous pouvons supposer en toute sécurité qu'il a les meilleures performances disponibles. En plus de cela, cette solution fournit le code le plus propre de tous.

9. Conclusion

Dans cet article, nous avons vu de nombreuses façons de résoudre un problème simple, y compris des erreurs. Nous les avons analysées pour trouver la meilleure solution pour chaque scénario.

Comme d'habitude, les exemples sont disponiblesover on GitHub.