1. El problema de ConcurrentModificationException
En esta lección no habrá (casi) nada nuevo. Pero es muy importante, porque eliminar datos de forma descuidada es uno de los errores más irreparables — especialmente en producción. Y, aun así, habrá una clase interesante que, con toda probabilidad, os encantará.
Así que sí, una y otra vez: ¡nada de for-each para eliminar! Por mucho que os guste.
Empecemos con un ejemplo clásico que provoca dolor y sufrimiento en muchos principiantes (y no solo en ellos):
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// Intentemos eliminar todos los números pares
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n); // ¡BOOM! ConcurrentModificationException
}
}
Parece que todo debería funcionar, pero en realidad el programa lanza una excepción:
Exception in thread "main" java.util.ConcurrentModificationException
Ahora veamos con más detalle qué ha pasado. Cuando recorres una colección con for-each (o con un Iterator normal), la colección mantiene internamente un «contador de modificaciones». Si durante el recorrido la colección cambia sin ser a través del propio iterador, ese contador detecta la «interferencia externa» y lanza una excepción. Es un mecanismo de protección para que el programa no trabaje con una estructura de datos corrupta.
2. Uso de Iterator
¿Cómo eliminar elementos correctamente durante el recorrido?
Recordatorio: Iterator es un objeto especial que permite recorrer una colección y eliminar elementos de forma segura «sobre la marcha». Es como un camarero que no solo sirve los platos, sino que también puede retirar un plato durante el recorrido por la mesa.
Obtener un iterador
Iterator<Integer> it = numbers.iterator();
Recorrido con while y eliminación con it.remove()
Este es el modo correcto de eliminar todos los números pares de una lista:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
Integer n = it.next();
if (n % 2 == 0) {
it.remove(); // Eliminación segura del elemento actual
}
}
System.out.println(numbers); // [1, 3, 5]
Punto importante: solo se puede eliminar a través del propio iterador (it.remove()) y solo después de llamar a it.next(). Si intentas invocar remove() dos veces seguidas sin next(), obtendrás IllegalStateException.
3. ListIterator: funcionalidades avanzadas
Y aquí está la novedad anunciada al principio de la lección: ListIterator es un iterador «potenciado» para listas (List) que permite no solo eliminar, sino también añadir elementos durante el recorrido, además de moverse en ambos sentidos (hacia delante y hacia atrás).
Diferencias con el Iterator normal
- Iterator — recto e implacable: solo hacia delante, solo eliminar.
- ListIterator — flexible y ágil: hacia delante y hacia atrás, eliminación, adición mediante add(), y además puedes sustituir el elemento actual con el método set().
Ejemplo: eliminación y adición de elementos
List<String> words = new ArrayList<>(List.of("cat", "dog", "bird"));
ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
String word = it.next();
if (word.length() == 3) {
it.remove(); // Eliminamos las palabras de 3 letras
it.add("pet"); // Justo después añadimos "pet" tras la palabra eliminada
}
}
System.out.println(words); // [pet, pet, bird]
Nota: la inserción con it.add() coloca el elemento inmediatamente después de la posición actual del iterador.
4. Eliminación con removeIf
Desde Java 8 existe un método conciso y cómodo: removeIf. Recibe una expresión lambda (o cualquier Predicate) y elimina todos los elementos para los que la condición devuelve true.
Ejemplo: eliminar todos los números pares
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
numbers.removeIf(n -> n % 2 == 0);
System.out.println(numbers); // [1, 3, 5]
No solo es más corto, sino también seguro: internamente el método usa el iterador adecuado — no habrá ningún ConcurrentModificationException.
Ejemplo: eliminar cadenas de menos de 3 caracteres
List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));
words.removeIf(word -> word.length() < 3);
System.out.println(words); // [cat, elephant]
Consejo: si lo que necesitas es simplemente eliminar elementos según una condición, usa removeIf. Es la forma más concisa y moderna.
5. Recomendaciones prácticas
¿Qué método es preferible?
- Si necesitas eliminar elementos por una condición (incluso compleja) y usas Java 8+: utiliza removeIf — corto, claro y seguro.
- Si estás en una versión antigua de Java o necesitas una lógica de recorrido más compleja: usa un Iterator y su método remove().
- Si trabajas con List y quieres no solo eliminar, sino también añadir elementos durante el recorrido: utiliza ListIterator.
Particularidades según el tipo de colección
- List: admite todos los enfoques descritos (Iterator, ListIterator, removeIf).
- Set: no hay índices, pero los Iterator estándar y removeIf funcionan.
- Map: para eliminar según condición, usa el iterador de entrySet():
Y con Java 8+ todo se simplifica muchísimo:Map<String, Integer> map = new HashMap<>(Map.of("a", 1, "b", 2, "c", 3)); Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Integer> entry = it.next(); if (entry.getValue() % 2 == 0) { it.remove(); } } System.out.println(map); // {a=1, c=3}map.entrySet().removeIf(entry -> entry.getValue() % 2 == 0);
6. Ejemplo práctico: filtrar usuarios
Supongamos que tenemos una lista de usuarios y queremos eliminar a todos los menores de 18 años.
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = new ArrayList<>(List.of(
new User("Anya", 17),
new User("Boris", 20),
new User("Vika", 15),
new User("Gleb", 25)
));
// Eliminamos a los menores con removeIf
users.removeIf(user -> user.age < 18);
System.out.println(users); // [Boris (20), Gleb (25)]
7. Comparación de enfoques
Hagamos una pequeña tabla para afianzar. A nuestro cerebro le gusta esto.
| Método | Disponible desde | Brevedad | Seguridad | Flexibilidad |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
Java 8+ | ++ | ✅ | + |
8. Errores típicos al eliminar elementos de colecciones
Error n.º 1: intento de eliminar elementos en for-each
for (String s : list) {
if (s.equals("test")) {
list.remove(s);
}
}
Ya lo sabes: así no se debe hacer — obtendrás ConcurrentModificationException. Usa un iterador o removeIf.
Error n.º 2: invocar remove() en el iterador sin next()
Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException — no se puede eliminar antes de llamar a next()
Error n.º 3: intento de eliminar elementos de una colección inmutable
List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException
Los métodos de eliminación no están soportados para colecciones inmutables.
Error n.º 4: intento de eliminar elementos de Map a través de values() o keySet() sin iterador
for (String key : map.keySet()) {
if (key.startsWith("a")) {
map.remove(key); // ConcurrentModificationException!
}
}
Usa un iterador sobre entrySet() o removeIf.
GO TO FULL VERSION