Remover todas as ocorrências de um valor específico de uma lista

Remover todas as ocorrências de um valor específico de uma lista

1. Introdução

Em Java, é simples remover um valor específico deList usandoList.remove(). No entanto,efficiently removing all occurrences of a value é muito mais difícil.

Neste tutorial, veremos várias soluções para esse problema, descrevendo os prós e os contras.

Para fins de legibilidade, usamos um métodolist(int…) personalizado nos testes, que retorna umArrayList contendo os elementos que passamos.

2. Usando um loopwhile

Já que sabemos comoremove a single element, doing it repeatedly in a loop parece bastante simples:

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

No entanto, não funciona como esperado:

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

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

O problema está na 3ª linha: chamamosList.remove(int), which treats its argument as the index, not the value we want to remove.

No teste acima, sempre chamamoslist.remove(1), mas o índice do elemento que queremos remover é0. ChamarList.remove() muda todos os elementos após o removido para índices menores.

Nesse cenário, significa que excluímos todos os elementos, exceto o primeiro.

Quando apenas o primeiro permanecer, o índice1 será ilegal. Portanto, obtemos umException.

Observe que enfrentamos esse problema apenas se chamarmosList.remove() com um argumento primitivobyte,short, char ouint, já que a primeira coisa que o compilador faz quando tenta encontrar o método sobrecarregado de correspondência está se ampliando.

Podemos corrigi-lo passando o valor comoInteger:

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

Agora o código funciona conforme o esperado:

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

// when
removeAll(list, valueToRemove);

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

ComoList.contains() eList.remove() precisam encontrar a primeira ocorrência do elemento, esse código causa travessia desnecessária do elemento.

Podemos fazer melhor se armazenarmos o índice da primeira ocorrência:

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

Podemos verificar se funciona:

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

// when
removeAll(list, valueToRemove);

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

Embora essas soluções produzam um código curto e limpo,they still have poor performance: porque não acompanhamos o progresso,List.remove() tem que encontrar a primeira ocorrência do valor fornecido para excluí-lo.

Além disso, quando usamos umArrayList, o deslocamento do elemento pode causar muitas cópias de referência, mesmo realocando a matriz de apoio várias vezes.

3. Removendo até as alterações deList

List.remove(E element) tem um recurso que ainda não mencionamos: elereturns a boolean value, which is true if the List changed because of the operation, therefore it contained the element.

Observe queList.remove(int index) retorna void, porque se o índice fornecido for válido,List sempre o remove. Caso contrário, ele lançaIndexOutOfBoundsException.

Com isso, podemos realizar remoções até que oList mude:

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

Funciona como esperado:

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

// when
removeAll(list, valueToRemove);

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

Apesar de curta, essa implementação sofre dos mesmos problemas que descrevemos na seção anterior.

3. Usando um loopfor

Podemos acompanhar nosso progresso percorrendo os elementos com um loopfor e remover o atual se corresponder a:

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

Funciona como esperado:

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

// when
removeAll(list, valueToRemove);

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

No entanto, se tentarmos com uma entrada diferente, ela fornecerá uma saída incorreta:

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

// when
removeAll(list, valueToRemove);

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

Vamos analisar como o código funciona, passo a passo:

  • i = 0

    • elementelist.get(i) são ambos iguais a1 na linha 3, então Java entra no corpo da instruçãoif,

    • removemos o elemento no índice0,

    • entãolist agora contém1,2e3

  • i = 1

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

Portanto, enfrentamos esse problema quandowe have two adjacent values, which we want to remove. Para resolver isso, devemos manter a variável de loop.

Diminuindo quando removemos o elemento:

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

Aumentando apenas quando não removemos o elemento:

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

Observe que, no último, removemos a instruçãoi++ na linha 2.

Ambas as soluções funcionam conforme o esperado:

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

// when
removeAll(list, valueToRemove);

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

Esta implementação parece correta à primeira vista. No entanto, ainda temserious performance problems:

  • remover um elemento de umArrayList, desloca todos os itens depois dele

  • acessar elementos por índice em umLinkedList significa percorrer os elementos um por um até encontrar o índice

4. Usando um loopfor-each

Desde Java 5, podemos usar o loopfor-each para iterar por meio de umList. Vamos usá-lo para remover elementos:

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

Observe que usamosInteger como o tipo da variável de loop. Portanto, não obteremos umNullPointerException.

Além disso, desta forma invocamosList.remove(E element), que espera o valor que queremos remover, não o índice.

Por mais limpo que pareça, infelizmente, não funciona:

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

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

O loopfor-each usaIterator para percorrer os elementos. No entanto,when we modify the List, the Iterator gets into an inconsistent state. Hence it throws ConcurrentModificationException.

A lição é: não devemos modificar umList, enquanto estamos acessando seus elementos em um loopfor-each.

5. Usando umIterator

Podemos usarIterator diretamente para percorrer e modificarList com ele:

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

Desta forma,the Iterator can track the state of the List (porque faz a modificação). Como resultado, o código acima funciona conforme o esperado:

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

// when
removeAll(list, valueToRemove);

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

Uma vez que cada classeList pode fornecer sua própria implementaçãoIterator, podemos assumir com segurança, queit implements element traversing and removal the most efficient way possible.

However, using ArrayList still means lots of element shifting (e talvez realocação da matriz). Além disso, o código acima é um pouco mais difícil de ler, porque difere do loopfor padrão, com o qual a maioria dos desenvolvedores está familiarizada.

6. Coletando

Até isso, modificamos o objetoList original removendo os itens de que não precisávamos. Em vez disso, podemoscreate 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;
}

Como fornecemos o resultado em um novo objetoList, temos que retorná-lo do método. Portanto, precisamos usar o método de outra maneira:

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

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

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

Observe que agora podemos usar o loopfor-each, uma vez que não modificamos oList que estamos iterando no momento.

Como não há remoções, não há necessidade de mudar os elementos. Portanto, esta implementação funciona bem quando usamos umArrayList.

Esta implementação se comporta de maneira diferente em alguns aspectos dos anteriores:

  • ele não modifica oList original, masreturns a new um

  • the method decides what the returned List‘s implementation is, pode ser diferente do original

Além disso, podemos modificar nossa implementação paraget the old behavior; limpamos oList original e adicionamos os elementos coletados a ele:

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

Funciona da mesma maneira que as anteriores:

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

// when
removeAll(list, valueToRemove);

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

Uma vez que não modificamos oList continuamente, não temos que acessar os elementos por posição ou deslocá-los. Além disso, existem apenas duas realocações de array possíveis: quando chamamosList.clear()eList.addAll().

7. Usando a API de Stream

O Java 8 introduziu expressões lambda e API de fluxo. Com esses recursos avançados, podemos resolver nosso problema com um código muito limpo:

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

Esta soluçãoworks the same way, like when we were collecting the remaining elements.

As a result, it has the same characteristics, e devemos usá-lo para retornar o resultado:

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

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

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

Observe que podemos convertê-lo para funcionar como as outras soluções com a mesma abordagem que fizemos com a implementação original de 'coleta'.

8. UsandoremoveIf

Com lambdas e interfaces funcionais, o Java 8 também introduziu algumas extensões de API. Por exemplo, oList.removeIf() method, which implements what we saw in the last section.

Ele espera umPredicate, que deve retornartrue when we want to remove o elemento, em contraste com o exemplo anterior, onde tivemos que retornartrue quando queríamos manter o elemento:

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

Funciona como as outras soluções acima:

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

// when
removeAll(list, valueToRemove);

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

Devido ao fato de que o próprioList implementa este método, podemos supor com segurança que ele possui o melhor desempenho disponível. Além disso, esta solução fornece o código mais limpo de todos.

9. Conclusão

Neste artigo, vimos várias maneiras de resolver um problema simples, incluindo erros incorretos. Nós os analisamos para encontrar a melhor solução para cada cenário.

Como de costume, os exemplos estão disponíveisover on GitHub.