Let's take a look at how, e.g., ArrayLists's iterator is implemented:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
public E next() {
checkForComodification();
int i = cursor;
if (i >= size) throw new NoSuchElementException();
// ...
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
// ...
ArrayList.this.remove(lastRet);
// ...
cursor = lastRet;
lastRet = -1;
}
Let's look at an example:
List list = new ArrayList(Arrays.asList(1, 2, 3, 4));
Iterator it = list.iterator();
Integer item = it.next();
We remove the first element
list.remove(0);
If we want to call it.remove() now, the iterator would remove number 2 because that's what field lastRet points to now.
if (item == 1) {
it.remove(); // list contains 3, 4
}
This would be incorrect behavior! The contract of the iterator states that remove() deletes the last element returned by next() but it couldn't hold its contract in the presence of concurrent modifications. Therefore it chooses to be on the safe side and throw an exception.
The situation may be even more complex for other collections. If you modify a HashMap, it may grow or shrink as needed. At that time, elements would fall to different buckets and an iterator keeping pointer to a bucket before rehashing would be completely lost.
Notice that iterator.remove() doesn't throw an exception by itself because it is able to update both the internal state of itself and the collection. Calling remove() on two iterators of the same instance collection would throw, however, because it would leave one of the iterators in an inconsistent state.