Modifying a collection while iterating through it is a common cause of bugs in Java. The language offers two distinct behaviors to address this: Fail-Fast and Fail-Safe iterators.
Fail-Fast iterators immediately throw a ConcurrentModificationException
if the collection is structurally modified. Fail-Safe iterators, on the other hand, tolerate such modifications by iterating over a copy of the collection.
In this tutorial, we will break down both concepts with code examples, performance implications, and best practices.
What are Fail-Fast and Fail-Safe Iterators?
Fail-Fast
- Detects structural modifications (add/remove) during iteration
- Throws
ConcurrentModificationException
- Found in most standard Java collections:
ArrayList
,HashMap
,HashSet
Fail-Safe
- Iterates over a snapshot of the collection
- Does not throw exceptions
- Found in
CopyOnWriteArrayList
,ConcurrentHashMap
Java Syntax and Examples
Fail-Fast Example
List<String> list = new ArrayList<>();
list.add("A"); list.add("B");
for (String item : list) {
list.add("C"); // Throws ConcurrentModificationException
}
Fail-Safe Example
CopyOnWriteArrayList<String> safeList = new CopyOnWriteArrayList<>();
safeList.add("X"); safeList.add("Y");
for (String item : safeList) {
safeList.add("Z"); // No exception, 'Z' won't be iterated
}
Internal Working and Memory Considerations
Fail-Fast
- Maintains a
modCount
(modification count) - Iterator checks for modification mismatch
if (expectedModCount != modCount) throw new ConcurrentModificationException();
Fail-Safe
- Uses a copy (CopyOnWriteArrayList) or lock-free snapshot (ConcurrentHashMap)
- More memory intensive but safe
Performance and Big-O Implications
Operation | Fail-Fast | Fail-Safe |
---|---|---|
Iteration Speed | Fast (direct) | Slower (copy-based) |
Memory Overhead | Low | High |
Thread-Safety | No | Yes (generally) |
Consistency | High | Eventual/stale |
Real-World Use Cases
Use Fail-Fast:
- Single-threaded applications
- Deterministic iteration logic
Use Fail-Safe:
- Multi-threaded environments (logging, analytics)
- Read-heavy workloads where consistency can lag
Functional Programming Impact (Java 8+)
Fail-Fast behavior also applies to streams and lambdas:
List<String> list = new ArrayList<>();
list.add("A"); list.add("B");
list.stream().forEach(item -> {
list.remove("A"); // Throws ConcurrentModificationException
});
Version Differences (Java 8–21)
Java 8
- Introduced
Stream
and lambda-based iteration (fail-fast by default)
Java 9
- Added factory methods (
List.of()
) which produce immutable collections
Java 21
- Enhanced performance in concurrent structures like
ConcurrentHashMap
Comparison Table
Feature | Fail-Fast | Fail-Safe |
---|---|---|
Exception on modify | Yes | No |
Thread-safe | No | Yes (depends on impl) |
Underlying strategy | modCount & check | Cloning / snapshot |
Memory usage | Low | High |
Iterator behavior | Consistent | Possibly stale |
Pros and Cons
Fail-Fast
✅ Fast and memory-efficient
❌ Unsafe in concurrent scenarios
Fail-Safe
✅ Safe in concurrent scenarios
❌ Slower and memory-heavy
Anti-Patterns and Refactoring Tips
❌ Modifying standard list in loop
for (String s : list) {
list.remove(s); // Fails fast
}
✅ Use Iterator.remove()
properly
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (condition) it.remove();
}
✅ Use concurrent collections
ConcurrentMap<String, String> cmap = new ConcurrentHashMap<>();
Best Practices for Safe Iteration
- Use Fail-Safe collections when working with concurrency
- Prefer
Iterator.remove()
for modifying while iterating - Avoid using
forEach
+ modification together - Consider
Collections.unmodifiableList()
for safe read-only operations
📌 What's New in Java [version]?
Java 8
- Stream API uses fail-fast iteration
Java 9
List.of()
creates immutable collections
Java 10
var
keyword for cleaner iterator code
Java 21
- Optimizations in thread-safe map iteration
Conclusion and Key Takeaways
- Fail-Fast is fast but fragile in multi-threaded scenarios
- Fail-Safe offers resilience but at a performance cost
- Understanding iterator behavior is critical for data consistency and avoiding bugs
- Always choose based on read/write ratio, thread safety, and memory budget
FAQ – 10 Expert-Level Questions
1. Can I modify a collection while iterating using a stream?
No. You’ll get a ConcurrentModificationException
.
2. How does CopyOnWriteArrayList
avoid fail-fast?
It creates a new copy of the array on each write.
3. Are all concurrent collections fail-safe?
Not necessarily. Always check the implementation.
4. Can fail-fast behavior be disabled?
No, it's baked into the iterator logic.
5. Is Enumeration
fail-fast?
No. Enumeration is legacy and not fail-fast.
6. Why is fail-fast not thread-safe?
Because it's designed to detect illegal concurrent modification, not prevent it.
7. What's a typical use of fail-safe iterators?
Iterating logs or metrics data in a multi-threaded analytics system.
8. Should I catch ConcurrentModificationException
?
No. It's a design-time signal, not for try-catch handling.
9. What’s safer: Iterator.remove() or List.remove()?
Iterator.remove()
within a loop is safe.
10. How do I create a truly immutable collection?
Use Collections.unmodifiableList()
or List.of()
from Java 9+.