JavaRush/Courses/Kotlin SELF/Safe removal while iterating collections

Safe removal while iterating collections

Open

1. Why removal during iteration often breaks your program

Imagine you are walking down a long corridor and counting doors. If someone, while you are counting, suddenly moves the walls, removes doors, and swaps them around, you will either lose your count, start skipping doors, or even bang your forehead (the exception plays the role of the forehead here). Collections are exactly the same: iteration is a process that assumes the data structure is not being “rebuilt” under your feet.

The main reason why removal during iteration is dangerous is that the for mechanism (via an iterator) expects the collection not to change “outside the rules”. Kotlin/JVM collections can often detect such interference and respond with a ConcurrentModificationException. Sometimes there is no exception, but a logical error appears: you remove an element, indices shift, and the next element “slips past the check”.

In this lecture, it is important to remember a practical formula: iteration and modification of a collection must be coordinated by the same mechanism. If you iterate with an iterator, the iterator must remove. If you iterate by indices, you must remove in a way that does not make the indices of not-yet-visited elements “shift around”.

The forbidden pattern: remove(...) inside for (x in list)

Let’s start with the most common “sin”, which looks logical, readable, and… sometimes works right up until the first real-world scenario. The logic feels rock-solid: “I iterate over elements; if an element is bad, I remove it.” But “iterate” and “remove” at that moment use different internal mechanisms, and they did not agree on the rules.

fun main() {
    val items = mutableListOf("keep", "remove", "keep")

    for (x in items) {
        if (x == "remove") {
            items.remove(x) // dangerous
        }
    }

    println(items)
}

On the JVM, such code often ends with ConcurrentModificationException: the iterator notices that the collection was modified “behind its back” and stops in the most honest way—by crashing. Even if you get lucky and no exception happens, you must not rely on it: the behavior depends on the collection implementation and the specific situation.

The key idea is this: for (x in items) uses an iterator under the hood, while items.remove(x) mutates the collection directly. It is like driving by a GPS while someone rearranges the roads in real time.

The forbidden pattern: removing by indices from left to right

The second trap is more common among people who already understood that “you can’t remove inside for (x in list)”, and decided: “Fine, I’ll iterate by index—I control everything!” Control does appear, but along with it comes a new problem: removal shifts elements to the left, while your index keeps growing to the right.

Let’s look at an example where we need to delete all "remove":

fun main() {
    val items = mutableListOf("keep", "remove", "remove", "keep")

    for (i in 0..items.lastIndex) {
        if (items[i] == "remove") {
            items.removeAt(i) // dangerous when moving left to right
        }
    }

    println(items)
}

Even if this code does not crash because lastIndex changes (and it can easily crash by going out of bounds), there is a subtler effect: when you remove items[1], the former items[2] moves into position 1. But the loop is already about to advance to i = 2, and you simply will not check that “moved” element. The result: some elements won’t be removed, and you get a bug without an exception. The most beloved kind of bug: “the program doesn’t crash, but it lies”.

To stop suffering like this, remember: if you remove by indices, you must go from the end to the beginning.

2. Strategy: removal via MutableIterator.remove()

Now comes the moment when collections suddenly become nice. Kotlin has a special iterator type for mutable collections: MutableIterator. It extends the regular Iterator and adds a remove() method that removes the “current” element correctly, i.e., in coordination with the iteration process.

The key idea: if iteration is driven by an iterator, then the iterator must do the removing.

Here is a safe variant for removing while iterating a list:

fun main() {
    val items = mutableListOf("a", "remove", "b")
    val it = items.iterator()

    while (it.hasNext()) {
        val x = it.next()
        if (x == "remove") {
            it.remove()
        }
    }

    println(items) // [a, b]
}

Notice the “ritual”: we first call next(), so the iterator “points” at the current element, and only after that can we call remove(). That is why people often say: remove() removes the last element that was returned by next().

The iterator is one-time use

After you reach the end, this iterator is “exhausted”. If you need to traverse the collection again, create a new iterator via items.iterator(). This is not a mistake or “wastefulness”; it is the normal iteration model.

Example from a mini-app: cleaning empty strings

Let’s keep the practical style: imagine that in a project we have a shopping list, and the user sometimes adds junk (empty strings, whitespace). We want to implement a cleanup command that removes empty entries.

fun cleanupBlankItems(items: MutableList<String>) {
    val it = items.iterator()

    while (it.hasNext()) {
        val name = it.next()
        if (name.isBlank()) {
            it.remove()
        }
    }
}

This function is small but useful: it shows exactly the case where removal happens “by condition” during iteration, and here the iterator is the most reliable tool.

The MutableIterator.remove() contract: how not to break the rules

The idea “remove via the iterator” sounds like salvation, but it comes with rules. And these rules are not “recommended”; they are “otherwise you will get an exception”. If you break the contract, collections become angry again—only now they will be angry for a good reason.

The most common problem is calling remove() twice in a row without a new next(). Here is what you must not do:

fun main() {
    val items = mutableListOf("remove", "x")
    val it = items.iterator()

    it.next()
    it.remove()

    // it.remove() // not allowed: there was no second next()
    println(items) // [x]
}

The logic is simple: after remove(), the iterator has already deleted the “last returned element”. To remove something again, the iterator must return another element via next(). If you try to remove again, the iterator does not understand “what exactly you want to remove” and rightfully complains.

In a real project, this usually surfaces not in such an explicit form, but when complex branching logic appears inside the loop. So a useful habit is to keep the loop body simple: “got an element → checked a condition → removed if needed”.

3. Strategy: removal by indices from right to left

Sometimes an iterator is not the most convenient option. For example, your logic depends on the index: you remove “every third element”, remove a range, or you need to print the element number to the user. In such cases it is easier to think in indices. But then you must remove in a way that does not affect the elements you have not checked yet.

The trick is that list indices go from 0 to lastIndex, where lastIndex == size - 1. If you go from the end to the beginning, removing an element does not shift the elements that are to the left. That means the indices of your “not yet checked” elements do not change.

Here is a safe example:

fun main() {
    val items = mutableListOf("keep", "remove", "remove", "keep")

    for (i in items.lastIndex downTo 0) {
        if (items[i] == "remove") {
            items.removeAt(i)
        }
    }

    println(items) // [keep, keep]
}

Mini-diagram: why right-to-left works

flowchart TD
    A[We go i from left to right] --> B["Remove items[i]"]
    B --> C[Elements to the right shift left]
    C --> D[Next i skips one element]
    D --> E[Logical error]

    A2[We go i from right to left] --> B2["Remove items[i]"]
    B2 --> C2[Only elements to the right shift, but everything on the right has already been checked]
    C2 --> D2[Unchecked elements on the left do not change index]
    D2 --> E2[Logic is correct]

4. How to choose a strategy in real code

In practice, you will choose a strategy not by aesthetics, but by what you need in a specific operation. To avoid keeping it all in your head as a “feeling”, it helps to have a small reminder table.

Situation Best fit Why
Remove elements by condition; index is not important
MutableIterator.remove()
Iteration and removal are done by the same “device”, fewer chances to make a mistake
Remove from MutableList, but logic depends on the index Loop for (i in lastIndex downTo 0) + removeAt(i) Indices of unchecked elements do not shift
Remove from MutableMap during iteration Iterator over entries + remove() Removing the “current entry” is coordinated with iteration
Remove one element “by value” without iterating
list.remove(value)
No iteration is needed at all, so there are no conflicts

Note: the table does not say “always do this”. It says: “if you are already in situation X, this is the most predictable path”.

5. Safe removal from MutableMap during iteration

A Map is not a list—you can’t traverse it by index. But sometimes you really want to remove from a MutableMap while iterating. For example, in our app we may have a MutableMap<String, Int> as a “warehouse”: product → quantity. And we want to clean out products whose quantity became 0.

If you try to do stock.remove(key) inside for ((k, v) in stock), you get the same conflict: “iterate via iterator, remove directly”. The correct option is an iterator over entries:

fun removeZeroStock(stock: MutableMap<String, Int>) {
    val it = stock.entries.iterator()

    while (it.hasNext()) {
        val entry = it.next()
        if (entry.value == 0) {
            it.remove()
        }
    }
}

Why entries? Because Map.Entry gives you both the key and the value right away, without a second lookup. And the iterator over entries can correctly remove the current pair. Conceptually, it is the same principle as with a list: the one who iterates should remove.

6. Integrating into an application: data cleanup operations

In real programs, removal during iteration rarely exists on its own. Usually it lives inside a “maintenance operation”: delete invalid records, remove empty elements, delete completed tasks, clean out zero stock. That is why it is useful to wrap such pieces into separate functions—small and testable.

Suppose that in our console application the shopping state is stored like this: each purchase is a Pair<String, Int>, where first is the product name and second is the quantity. (We have not covered classes yet, so no data class.)

Let’s write a function that removes purchases whose quantity is <= 0. This is a classic case of “remove many elements by condition”, so an iterator is ideal:

fun removeNonPositive(items: MutableList<Pair<String, Int>>) {
    val it = items.iterator()

    while (it.hasNext()) {
        val item = it.next()
        if (item.second <= 0) {
            it.remove()
        }
    }
}

Now a situation where indices are more convenient: suppose we want to remove all elements named "tmp" (the user added temporary entries), but we also want to print which lines we removed, with their “number in the list at the moment of removal”. Then we can go right to left, and the numbers to the “right” of us no longer matter:

fun removeAllTmp(items: MutableList<String>) {
    for (i in items.lastIndex downTo 0) {
        if (items[i] == "tmp") {
            items.removeAt(i)
        }
    }
}

It is important here not to try to “make it prettier” by switching to left-to-right traversal: right-to-left is not a whim; it is a correctness guarantee.

remove(value) vs removeAt(index): don’t mix up the meaning

When you write code that “removes”, it is very easy to start confusing: are you removing by value or by position? This is especially noticeable in command-driven apps, where the user enters remove 3 and expects “remove the third element”, but you accidentally do “remove the element with value "3"”.

A MutableList typically has two different operations: remove by value and remove by index. In Kotlin, this is expressed right in the names: removeAt(i) removes by position, while remove(x) removes by value. When you design commands, choose the semantics in advance: either the remove command removes by number (then you need removeAt and a check i in indices), or it removes by value (then remove(value)), or you have two different commands.

This matters specifically in today’s context: if you remove “many elements” by condition, almost always you will either call removeAt many times, or use an iterator. But if you remove exactly one element by value, you do not need iteration at all.

7. Common mistakes in safe removals

Mistake #1: removing via the collection inside for (x in collection).
This looks natural, but it conflicts with how for works: under the hood it uses an iterator, while you mutate the collection structure directly. A common outcome is ConcurrentModificationException, and sometimes even worse: “just weird behavior”. A reliable solution is to remove either via MutableIterator.remove(), or not remove during iteration at all.

Mistake #2: iterating by indices left to right and doing removeAt(i) inside the loop.
Technically, you “control everything”, but removal shifts elements, and you can easily skip part of the collection or go out of bounds. If you choose the index-based style, there is one safe rule: remove right to left via for (i in lastIndex downTo 0).

Mistake #3: calling it.remove() against the contract, e.g., twice in a row.
MutableIterator.remove() removes the element that was last returned by next(). If you call remove() before next() or repeat remove() without a new next(), the iterator cannot correctly determine the removal target and will throw an exception. The discipline is simple: “next() → check → maybe remove()”.

Mistake #4: trying to remove from a Map by iterating over keys and simultaneously doing map.remove(key).
Even if it seems like “I remove by key, that’s logical”, the problem is the same: iteration and removal use different mechanisms. The reliable path: val it = map.entries.iterator() and remove via it.remove().

Mistake #5: mixing up “index” and “user-facing number”.
A user thinks “first element” and means 1, while a list thinks “first element” and means index 0. If you accept a deletion command by number, always do index = number - 1 and check index in items.indices, otherwise you will either delete the wrong element or crash.

1
Task
Kotlin SELF,  20 level,  3 lecture
Locked
Notes cleanup
Notes cleanup
1
Task
Kotlin SELF,  20 level,  3 lecture
Locked
Measurement Cleanup
Measurement Cleanup
1
Task
Kotlin SELF,  20 level,  3 lecture
Locked
Step filter
Step filter
1
Task
Kotlin SELF,  20 level,  3 lecture
Locked
Warehouse cleanup
Warehouse cleanup
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet