CodeGym /Cursos /JAVA 25 SELF /Eliminación segura de elementos

Eliminación segura de elementos

JAVA 25 SELF
Nivel 28 , Lección 2
Disponible

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():
    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}
    
    Y con Java 8+ todo se simplifica muchísimo:
    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
for-each + remove()
Java 5+ - -
Iterator + remove()
Java 5+ + +
ListIterator
Java 5+ + ++
removeIf
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.

1
Tarea
JAVA 25 SELF, nivel 28, lección 2
Bloqueada
Recualificación de mascotas en la tienda de animales "Amigo" 🐶
Recualificación de mascotas en la tienda de animales "Amigo" 🐶
1
Tarea
JAVA 25 SELF, nivel 28, lección 2
Bloqueada
Limpieza de la lista de participantes del concurso de resultados 'sospechosos' 🏅
Limpieza de la lista de participantes del concurso de resultados 'sospechosos' 🏅
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION