Java Iterator
Toda coleção do Java pode ser percorrida, um elemento por vez, por um Iterator (iterador): um "cursor" que sabe onde está e qual é o próximo. Você já usa iteradores sem saber, porque o for-each é feito com eles. Nesta aula você vai usá-los diretamente, e vai descobrir por que remover elementos dentro de um for-each quebra o programa com ConcurrentModificationException, e como fazer isso do jeito certo.
- O que é um
Iteratore os seus três métodos:hasNext,nexteremove. - Como o for-each usa um iterador por baixo dos panos.
- Por que remover dentro de um for-each dá
ConcurrentModificationException(e às vezes, pior, não dá). - As três formas certas de remover enquanto percorre:
Iterator.remove,removeIfe o laço de trás para a frente. - O mesmo problema (e as mesmas soluções) em mapas.
A ideia: o dedo na lista de chamada
Quando a Ana faz a chamada, ela põe o dedo antes do primeiro nome e repete: "tem mais alguém?"; se tem, desce o dedo e lê o nome. Um iterador é exatamente esse dedo:
| Método | Tradução | Na chamada |
|---|---|---|
hasNext() | "tem próximo?" | "Tem mais alguém abaixo do meu dedo?" Devolve true ou false, sem mexer o dedo. |
next() | "próximo" | Desce o dedo um nome e devolve esse nome. |
remove() | "remover" | Risca da lista o nome que o dedo acabou de ler. |
Percorrer com um Iterator
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> alunos = new ArrayList<>(List.of("Ana", "Bruno", "Carla"));
Iterator<String> it = alunos.iterator();
while (it.hasNext()) {
String aluno = it.next();
System.out.println("Chamando " + aluno);
}
System.out.println("Ainda tem? " + it.hasNext());
}
}
- import java.util.Iterator;
- O
Iteratortambém mora emjava.util. - Iterator<String> it = alunos.iterator();
- Pede à lista um iterador novo. O dedo começa antes do primeiro elemento. O
<String>diz que onext()vai devolverString. - while (it.hasNext())
- "Enquanto houver um próximo…" (veja while).
- String aluno = it.next();
- Avança e pega o elemento. Chame
next()uma vez por volta: cada chamada avança o dedo.
$ javac Main.java $ java Main Chamando Ana Chamando Bruno Chamando Carla Ainda tem? false « o dedo chegou ao fim
Esse mesmo código funciona com qualquer coleção: troque a lista por um HashSet ou um TreeSet e nada mais muda. É por isso que o iterador existe: um jeito único de percorrer coleções que guardam os dados de formas muito diferentes (um array, nós encadeados, uma árvore…). Conjuntos não têm get(i), mas todos têm iterator().
Um iterador só anda para a frente e só serve uma vez. Para percorrer de novo, peça outro com alunos.iterator().
O for-each é um iterador disfarçado
Quando você escreve um for-each numa coleção, o compilador o transforma no while com iterador acima. Estes dois trechos são equivalentes:
for (String aluno : alunos) { Iterator<String> it = alunos.iterator();
System.out.println(aluno); while (it.hasNext()) {
} String aluno = it.next();
System.out.println(aluno);
}
Qualquer classe que tenha o método iterator() (ou seja, que implemente a interface Iterable) pode ser usada num for-each. Guarde esse detalhe: ele explica o erro da próxima seção.
next() a mais: NoSuchElementException
Este código tem um erro de propósito.
List<String> alunos = new ArrayList<>(List.of("Ana", "Bruno"));
Iterator<String> it = alunos.iterator();
System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next());
$ java Main Ana Bruno Exception in thread "main" java.util.NoSuchElementException at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1053) at Main.main(Main.java:11)
O que quer dizer: "não há tal elemento": o terceiro next() passou do fim. (ArrayList$Itr é o nome da classe do iterador da ArrayList: Itr é uma classe interna dela.)
Como consertar: sempre pergunte hasNext() antes de cada next().
O problema: remover dentro de um for-each
A Ana quer tirar da lista as notas abaixo de 5. O jeito que parece óbvio:
Este código tem um erro de propósito.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> notas = new ArrayList<>(List.of(8, 3, 9, 4, 7));
for (Integer n : notas) {
if (n < 5) {
notas.remove(n);
}
}
System.out.println(notas);
}
}
$ javac Main.java $ java Main Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050) at Main.main(Main.java:7)
ConcurrentModificationException quer dizer "exceção de modificação concorrente": a lista foi modificada ao mesmo tempo em que era percorrida. Leia a pilha de baixo para cima:
Main.main(Main.java:7): a linha 7 é a dofor. O erro não aconteceu noremove(linha 9), mas na volta seguinte do laço.ArrayList$Itr.next: o for-each chamou onext()do iterador escondido.checkForComodification: "verificar se houve modificação". O iterador percebeu que a lista mudou sem ele saber, e parou.
Por que o iterador reclama? Lembre do dedo na lista de chamada. Se alguém risca um nome pelas costas da Ana, os nomes de baixo sobem uma linha e o dedo dela fica apontando para o lugar errado: ela pularia alguém sem perceber. Para evitar esse bug silencioso, a ArrayList conta quantas vezes foi modificada, e o iterador confere essa contagem a cada next(). Se mudou por fora, ele lança a exceção (o nome técnico desse comportamento é fail-fast, "falhar rápido").
Repare: notas.remove(n) aqui remove pelo valor, porque n é um Integer (e não um int); veja a pegadinha do remove. O problema não está no remove, e sim em quem remove: a lista, e não o iterador.
Com outros dados, o mesmo código pode terminar sem erro, e errado. Com a lista [8, 9, 3, 4]:
$ java Main [8, 9, 4] « o 4 deveria ter saído!
O 3 era o penúltimo. Ao removê-lo, a lista ficou com 3 elementos, e o for-each, que já tinha passado por 3, achou que tinha terminado: nem chegou a chamar o next() que faria a verificação. O 4 nunca foi examinado. Por isso a regra é absoluta: nunca altere uma coleção dentro de um for-each sobre ela, nem quando "funciona".
Solução 1: remover pelo próprio iterador
Se é o dedo que risca o nome, ele sabe o que aconteceu e se ajusta. Para isso, escreva o laço com o iterador explícito e chame it.remove():
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> notas = new ArrayList<>(List.of(8, 3, 9, 4, 7));
Iterator<Integer> it = notas.iterator();
while (it.hasNext()) {
int n = it.next();
if (n < 5) {
it.remove();
}
}
System.out.println(notas);
}
}
$ javac Main.java $ java Main [8, 9, 7]
- int n = it.next();
- Lê o próximo. Aqui guardamos num
int(o Java desembrulha oInteger). - it.remove();
- Remove o último elemento que o
next()devolveu. Não recebe argumento: o iterador já sabe qual é.
it.remove()
Ele só pode ser chamado depois de um next(), e uma vez por next(). Chamar antes de qualquer next(), ou duas vezes seguidas, dá IllegalStateException (veja Quando dá errado).
Missão: o programa quer tirar da turma quem tem nome começando com "B", mas quebra com ConcurrentModificationException. Reescreva o laço com um Iterator e it.remove(). A saída deve ser [Ana, Carla].
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> alunos = new ArrayList<>(List.of("Ana", "Bruno", "Beatriz", "Carla"));
for (String a : alunos) {
if (a.startsWith("B")) {
alunos.remove(a);
}
}
System.out.println(alunos);
}
}
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> alunos = new ArrayList<>(List.of("Ana", "Bruno", "Beatriz", "Carla"));
Iterator<String> it = alunos.iterator();
while (it.hasNext()) {
String a = it.next();
if (a.startsWith("B")) {
it.remove();
}
}
System.out.println(alunos);
}
}
Solução 2: removeIf
Desde o Java 8, toda coleção tem removeIf ("remover se"): você diz a condição, e a coleção remove todos os elementos que a satisfazem, do jeito seguro. Na prática, é a forma mais usada hoje:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> notas = new ArrayList<>(List.of(8, 3, 9, 4, 7));
boolean mudou = notas.removeIf(n -> n < 5);
System.out.println(mudou + " " + notas);
}
}
$ javac Main.java $ java Main true [8, 9, 7] « true = alguma coisa foi removida
- n -> n < 5
- Uma lambda: uma pequena função que recebe cada elemento (
n) e respondetrue("remova") oufalse("mantenha"). Lê-se "dado n, n menor que 5". - boolean mudou
removeIfdevolvetruese removeu pelo menos um.
Uma linha só, impossível de errar o índice e, por dentro, mais rápido que remover um por um numa ArrayList. Use o iterador explícito quando, além de remover, você precisa fazer outras coisas no mesmo laço (mostrar quem saiu, contar, etc.).
Solução 3: laço com índice, de trás para a frente
Em listas, dá para usar um for com índice. Mas atenção à direção. Para a frente, ele erra sem avisar:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> notas = new ArrayList<>(List.of(8, 3, 4, 9));
for (int i = 0; i < notas.size(); i++) {
if (notas.get(i) < 5) {
notas.remove(i);
}
}
System.out.println("para a frente: " + notas);
List<Integer> notas2 = new ArrayList<>(List.of(8, 3, 4, 9));
for (int i = notas2.size() - 1; i >= 0; i--) {
if (notas2.get(i) < 5) {
notas2.remove(i);
}
}
System.out.println("de trás para a frente: " + notas2);
}
}
$ javac Main.java $ java Main para a frente: [8, 4, 9] « o 4 escapou de trás para a frente: [8, 9] « certo
Para a frente: com i = 1, o 3 sai e o 4 sobe para a posição 1. Aí o i++ pula para 2, e o 4 nunca é examinado. De trás para a frente, os elementos que se movem são os que já foram examinados, então ninguém escapa. (Aqui notas.remove(i) é por posição, porque i é int.)
Qual usar?
| Forma | Quando usar |
|---|---|
colecao.removeIf(x -> condição) | O padrão: só remover quem satisfaz uma condição. Funciona em listas, conjuntos e nas visões de mapas. |
Iterator + it.remove() | Remover e fazer outras coisas no mesmo laço (mostrar, contar, decidir com várias regras). |
for com índice, de trás para a frente | Listas, quando você precisa da posição. Nunca para a frente. |
| Criar uma coleção nova só com quem fica | Quando a original não pode mudar (por exemplo, com streams: filter). |
for-each + colecao.remove | Nunca. |
O mesmo problema em mapas
Mapas também são percorridos por iteradores (das visões keySet, values e entrySet), e o problema se repete. A Ana quer tirar do estoque os produtos zerados:
Este código tem um erro de propósito.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> estoque = new HashMap<>();
estoque.put("maçã", 12);
estoque.put("uva", 0);
estoque.put("banana", 30);
estoque.put("kiwi", 0);
for (String produto : estoque.keySet()) {
if (estoque.get(produto) == 0) {
estoque.remove(produto);
}
}
System.out.println(estoque);
}
}
$ javac Main.java $ java Main Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1606) at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1629) at Main.main(Main.java:11)
O erro é o mesmo, agora vindo do iterador de chaves do HashMap (KeyIterator). A solução mais curta é o removeIf numa das visões. Remover da visão remove do mapa:
estoque.values().removeIf(qtd -> qtd == 0); // pelo valor
estoque.keySet().removeIf(produto -> produto.startsWith("k")); // pela chave
estoque.entrySet().removeIf(par -> par.getValue() == 0); // pelos dois
Com a primeira linha, num TreeMap (para a saída sair em ordem):
$ java Main
{banana=30, maçã=12}
Quando dá errado
ConcurrentModificationException
Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050) at Main.main(Main.java:7)
O que quer dizer: a coleção foi alterada (com add ou remove dela mesma) enquanto um iterador, quase sempre o de um for-each, a percorria. A linha apontada é a do for, não a do remove.
Como consertar: removeIf, it.remove() ou laço de trás para a frente. Para acrescentar durante o percurso, junte os novos numa lista separada e faça addAll depois do laço.
Apesar do nome "concorrente", isso não tem nada a ver com threads: acontece com um programa de uma linha de execução só.
IllegalStateException: remove() antes do next()
Este código tem um erro de propósito.
List<String> alunos = new ArrayList<>(List.of("Ana", "Bruno"));
Iterator<String> it = alunos.iterator();
it.remove();
$ java Main Exception in thread "main" java.lang.IllegalStateException at java.base/java.util.ArrayList$Itr.remove(ArrayList.java:1063) at Main.main(Main.java:9)
O que quer dizer: "estado ilegal": o iterador ainda não leu ninguém, então não sabe o que remover. O mesmo acontece se você chamar it.remove() duas vezes depois do mesmo next().
Como consertar: um it.remove() por it.next(), sempre depois dele.
O que aconteceu: ou você removeu dentro de um for-each e o elemento removido era o penúltimo, ou usou um for com índice para a frente. Nos dois casos, um elemento foi pulado.
Como consertar: troque por removeIf.
Referência
| Código | O que faz |
|---|---|
Iterator<T> it = colecao.iterator(); | Cria um iterador, antes do primeiro elemento |
it.hasNext() | Tem próximo? (não avança) |
it.next() | Avança e devolve; passou do fim: NoSuchElementException |
it.remove() | Remove o último devolvido por next(); fora de hora: IllegalStateException |
colecao.removeIf(x -> cond) | Remove todos que satisfazem cond; devolve true se removeu |
mapa.values().removeIf(…), keySet(), entrySet() | Remove pares do mapa pelo valor, pela chave ou pelos dois |
list.listIterator() | Iterador de lista: também anda para trás (hasPrevious/previous) e troca (set) |
Iterable<T> | Interface de quem tem iterator(): pode ir num for-each |
Exercícios
1. Tem mais?
Complete a condição do laço:
2. Remover com segurança
Complete para remover da lista nomes todos os nomes vazios:
3. Em que linha a ConcurrentModificationException de um for-each é apontada?
4. O que it.remove() remove?
5. Num for (int i = 0; i < l.size(); i++) que remove com l.remove(i), o que pode acontecer?
Missão final: tire do estoque os produtos com quantidade 0, usando removeIf numa das visões do mapa (values() ou entrySet()). A saída deve ser {banana=30, maçã=12}.
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
Map<String, Integer> estoque = new TreeMap<>();
estoque.put("maçã", 12);
estoque.put("uva", 0);
estoque.put("banana", 30);
estoque.put("kiwi", 0);
// remova os zerados
System.out.println(estoque);
}
}
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
Map<String, Integer> estoque = new TreeMap<>();
estoque.put("maçã", 12);
estoque.put("uva", 0);
estoque.put("banana", 30);
estoque.put("kiwi", 0);
estoque.values().removeIf(qtd -> qtd == 0);
System.out.println(estoque);
}
}
Resumo
- Um
Iteratorpercorre qualquer coleção:hasNext()pergunta,next()avança e devolve,remove()tira o último lido. - O for-each usa um iterador escondido.
- Alterar a coleção dentro do for-each dá
ConcurrentModificationException, ou, pior, pula elementos sem avisar. - Para remover enquanto percorre:
removeIf(condição)(o padrão),it.remove(), ouforcom índice de trás para a frente. - Em mapas:
values().removeIf,keySet().removeIf,entrySet().removeIf. next()além do fim:NoSuchElementException;remove()fora de hora:IllegalStateException.
Próxima aula: A Classe Collections, a caixa de ferramentas com sort, shuffle, max e companhia.