Detailed explanation
Fail-fast and “fail-safe” describe how iteration behaves when a collection changes during traversal. Fail-fast iterators, such as those returned by ArrayList and HashMap, attempt to detect unsupported structural modifications and throw ConcurrentModificationException. Structural changes generally add or remove elements or mappings; replacing an existing value is usually not structural. The exception can occur in a single thread, so “concurrent modification” does not necessarily mean multiple threads are involved. This distinction matters when choosing collections and writing safe traversal logic.
Under the hood, many fail-fast implementations maintain a modification counter, commonly called modCount. An iterator records the expected counter and checks it at certain operations, such as next(). If the counters differ, it throws ConcurrentModificationException. Calling a supported Iterator.remove() updates both the collection and the iterator’s expected state, allowing that iterator to continue. In the supplied enhanced-for loop, iteration uses an iterator internally, but l.remove(0) modifies the list directly; the next iterator check therefore throws. Detection is best-effort, not guaranteed, and must never be used as a thread-safety or correctness mechanism.
“Fail-safe” is informal terminology, not a standard Java iterator contract. Importantly, the examples do not all iterate over a clone or snapshot. CopyOnWriteArrayList provides snapshot iterators: an iterator retains the backing array that existed when it was created, while mutating operations publish a new array as needed. Consequently, later changes are invisible to that iterator, and its remove(), set(), and add() operations are unsupported. Copying makes writes expensive, so this collection is most suitable when traversal greatly outnumbers updates.
ConcurrentHashMap instead provides weakly consistent iterators. They tolerate concurrent updates without throwing ConcurrentModificationException and may reflect some updates made after iterator creation; they do not provide a fixed, point-in-time snapshot. Its iterator supports removal, unlike CopyOnWriteArrayList’s iterator. Interviewers typically look for these precise consistency guarantees, the difference between direct collection mutation and supported iterator mutation, and practical trade-offs. A strong answer therefore distinguishes fail-fast detection, snapshot traversal, and weakly consistent traversal rather than treating every non-fail-fast iterator as snapshot-based.
Key points to remember
- ArrayList and HashMap iterators detect unsupported structural changes on a best-effort basis.
- ConcurrentModificationException can occur even when only one thread is executing.
- Supported Iterator.remove() is different from directly removing through the collection.
- CopyOnWriteArrayList iterators use a fixed snapshot and do not support iterator mutation.
- ConcurrentHashMap iterators are weakly consistent, not snapshot-based; “fail-safe” is informal terminology.
Common mistakes candidates make
- Claiming that every “fail-safe” iterator works on a cloned collection.
- Treating ConcurrentModificationException as guaranteed detection or a thread-safety mechanism.
- Assuming every update is structural, including ArrayList.set() or replacing an existing HashMap value.
Likely follow-up questions
How can you safely remove elements while iterating over an ArrayList?
Use an explicit iterator and call its remove() after next(), at most once per next() call. For predicate-based removal, removeIf() is usually clearer; neither approach makes unsynchronised concurrent access safe.
Does wrapping a list with Collections.synchronizedList() make iteration automatically safe?
No: individual operations are synchronised, but traversal is a sequence of operations. Synchronise on the returned wrapper for the entire traversal, and ensure other access uses the same synchronisation discipline.
When would you choose CopyOnWriteArrayList instead of a conventionally locked list?
Choose it when reads and traversals greatly outnumber writes and snapshot visibility is acceptable. Writes incur array-copying costs, and long-lived iterators can retain older arrays in memory.
Interview tip: Start with fail-fast detection, then explicitly separate CopyOnWriteArrayList’s snapshot behaviour from ConcurrentHashMap’s weak consistency.
