Java Tutorial
🔍

Java Iterator

Java Iterator

java.util.Iterator<E> is the interface that all Java collections use for traversal. Every time a for-each loop runs on an ArrayList, a HashSet, or a TreeMap's key set, an Iterator is doing the actual work behind the scenes. Understanding it — not just the syntax, but the internal mechanism — is what lets you remove elements safely during traversal, write custom traversal logic, and diagnose ConcurrentModificationException when it fires.

What Is Java Iterator?

Iterator<E> is an interface in java.util with three methods. Two are abstract, one is default:

public interface Iterator<E> {
    boolean hasNext();            ← is there a next element?
    E       next();               ← return current and advance cursor
    default void remove() {       ← remove the last element returned by next()
        throw new UnsupportedOperationException();
    }
}

The diagram below shows where Iterator sits relative to Iterable and the Collection hierarchy.

ITERATOR IN THE COLLECTIONS HIERARCHY

java.lang.Iterable<E>
    │   method: Iterator<E> iterator()   ← produces an Iterator
    │
    └── java.util.Collection<E>
            ├── List  → ArrayList, LinkedList
            ├── Set   → HashSet, TreeSet
            └── Queue → ArrayDeque, PriorityQueue

java.util.Iterator<E>                   ← the traversal cursor
    │   methods: hasNext(), next(), remove()
    │
    └── java.util.ListIterator<E>        ← bidirectional, for List only
            extra: hasPrevious(), previous(), add(), set()

Relationship:
  myList.iterator()  → returns a fresh Iterator starting at position 0
  for (E item : myList) → compiler expands to an Iterator loop

Every Collection implementation returns its own Iterator subclass from iterator(). For ArrayList, that is an inner class called Itr that tracks an integer index. For LinkedList, it is a Node pointer. For HashSet, it scans through the backing hash table's bucket array.

Key Behaviours at a Glance

  • Calling next() without a prior hasNext() check throws NoSuchElementException when the cursor is exhausted
  • Calling remove() without a prior next() call throws IllegalStateException
  • Most standard collection iterators are fail-fast — structural modification outside the iterator throws ConcurrentModificationException
  • Each call to collection.iterator() creates a fresh, independent cursor starting at position 0

When to Use Iterator

The decision between for-each, Iterator, and other traversal options follows a straightforward rule:

USE for-each when:
  - Read-only traversal
  - No removal or index access needed
  - Cleanest, most readable code

USE explicit Iterator when:
  - Removing elements during traversal — iterator.remove() is the ONLY safe way
  - Need to know when you are at the last element and act differently
  - Building APIs that accept any Iterable and iterate generically

USE ListIterator when:
  - Backward traversal on a List
  - In-place replacement with set() during traversal
  - Inserting elements at the current position with add()

USE Streams (Java 8+) when:
  - Filtering, mapping, or collecting results
  - Parallel processing is an option
  - Functional-style pipeline is more readable than a loop

DO NOT use explicit Iterator for:
  - Pure read operations — for-each is cleaner
  - Index-based access on a List — use a standard for(int i...) loop

How Iterator Works Internally

Understanding the internal mechanism is what separates a developer who uses Iterator from one who can reason about it under pressure.

ArrayList's Iterator — modCount and the Fail-Fast Mechanism

ArrayList keeps an internal counter called modCount that increments on every structural modification — every add(), remove(), and clear() call. When an Iterator is created via list.iterator(), it snapshots this counter as expectedModCount. Every next() call checks whether modCount == expectedModCount.

ARRAYLIST ITERATOR INTERNAL STATE:

  ArrayList internal fields:
    Object[] elementData    ← the backing array
    int      size           ← number of live elements
    int      modCount       ← increments on every structural change

  ArrayList.Itr (iterator) internal fields:
    int cursor              ← index of next element to return (starts at 0)
    int lastRet = -1        ← index of last element returned by next()
    int expectedModCount    ← snapshot of modCount at iterator creation

  next() execution:
    1. Check modCount == expectedModCount → throw CME if different
    2. Check cursor < size              → throw NSEE if exhausted
    3. result = elementData[cursor]
    4. lastRet = cursor
    5. cursor++
    6. return result

  remove() execution:
    1. Check lastRet >= 0   → throw ISE if next() not called first
    2. Check modCount == expectedModCount → throw CME if different
    3. ArrayList.this.remove(lastRet)   ← actual removal
    4. cursor = lastRet                 ← adjust cursor for shift
    5. lastRet = -1                     ← reset to prevent double-remove
    6. expectedModCount = modCount      ← sync after internal modification

The key insight in step 6 of remove(): after calling iterator.remove(), the iterator updates its own expectedModCount to match the new modCount. This is why iterator.remove() is safe — the iterator accounts for its own structural changes.

When you call list.remove(element) directly inside a for-each loop, that increments modCount but the iterator's expectedModCount is not updated. The next next() call detects the mismatch and throws ConcurrentModificationException.

Core Operations with Examples

Basic Traversal — hasNext() and next()

The foundation of all Iterator usage. Every for-each loop compiles to exactly this pattern.

1// File: IteratorBasicsDemo.java 2 3import java.util.ArrayList; 4import java.util.Iterator; 5import java.util.List; 6 7public class IteratorBasicsDemo { 8 9 public static void main(String[] args) { 10 11 List<String> platforms = new ArrayList<>(); 12 platforms.add("Swiggy"); 13 platforms.add("Zomato"); 14 platforms.add("Blinkit"); 15 platforms.add("Zepto"); 16 platforms.add("Dunzo"); 17 18 // Explicit iterator — what the compiler generates for for-each 19 System.out.println("=== Explicit Iterator ==="); 20 Iterator<String> it = platforms.iterator(); 21 while (it.hasNext()) { 22 String platform = it.next(); // advance cursor and return element 23 System.out.println(" " + platform); 24 } 25 26 // for-each — identical behaviour, cleaner syntax 27 System.out.println("\n=== for-each (same cursor under the hood) ==="); 28 for (String platform : platforms) { 29 System.out.println(" " + platform); 30 } 31 32 // Two independent iterators on the same list — each has its own cursor 33 System.out.println("\n=== Two independent iterators ==="); 34 Iterator<String> first = platforms.iterator(); 35 Iterator<String> second = platforms.iterator(); 36 System.out.println("first.next() = " + first.next()); // Swiggy 37 System.out.println("first.next() = " + first.next()); // Zomato 38 System.out.println("second.next() = " + second.next()); // Swiggy — independent 39 System.out.println("first still has more: " + first.hasNext()); // true 40 } 41}
Output:
=== Explicit Iterator ===
  Swiggy
  Zomato
  Blinkit
  Zepto
  Dunzo

=== for-each (same cursor under the hood) ===
  Swiggy
  Zomato
  Blinkit
  Zepto
  Dunzo

=== Two independent iterators ===
first.next()  = Swiggy
first.next()  = Zomato
second.next() = Swiggy — independent
first still has more: true

Safe Removal During Traversal — iterator.remove()

This is the primary reason to use an explicit Iterator in production code. removeIf() is the cleaner Java 8+ alternative for simple predicates, but explicit iterator.remove() remains important when the removal logic depends on state accumulated during traversal.

1// File: IteratorRemoveDemo.java 2 3import java.util.ArrayList; 4import java.util.Iterator; 5import java.util.List; 6 7public class IteratorRemoveDemo { 8 9 public static void main(String[] args) { 10 11 List<Integer> orderAmounts = new ArrayList<>( 12 List.of(250, 1200, 80, 3500, 150, 4200, 60, 900) 13 ); 14 System.out.println("Original: " + orderAmounts); 15 16 // WRONG: modifies list inside for-each — throws ConcurrentModificationException 17 // for (Integer amount : orderAmounts) { 18 // if (amount < 200) orderAmounts.remove(amount); // CME here 19 // } 20 21 // CORRECT: iterator.remove() is structurally safe 22 Iterator<Integer> it = orderAmounts.iterator(); 23 while (it.hasNext()) { 24 int amount = it.next(); 25 if (amount < 200) { 26 it.remove(); // iterator adjusts cursor and syncs modCount 27 } 28 } 29 System.out.println("After removing orders below Rs.200: " + orderAmounts); 30 31 // Modern alternative: removeIf (Java 8+) — delegates to iterator internally 32 List<Integer> copy = new ArrayList<>(List.of(250, 1200, 80, 3500, 150, 4200, 60, 900)); 33 copy.removeIf(amount -> amount < 200); 34 System.out.println("removeIf equivalent : " + copy); 35 36 // Remove the last element returned — requires calling next() first 37 List<String> tags = new ArrayList<>(List.of("java", "spring", "devops", "aws", "java")); 38 System.out.println("\nBefore de-dup: " + tags); 39 Iterator<String> tagIt = tags.iterator(); 40 String previous = null; 41 while (tagIt.hasNext()) { 42 String current = tagIt.next(); 43 if (current.equals(previous)) { 44 tagIt.remove(); // remove consecutive duplicates 45 } 46 previous = current; 47 } 48 System.out.println("After removing consecutive dups: " + tags); 49 } 50}
Output:
Original: [250, 1200, 80, 3500, 150, 4200, 60, 900]
After removing orders below Rs.200: [250, 1200, 3500, 4200, 900]
removeIf equivalent            : [250, 1200, 3500, 4200, 900]

Before de-dup: [java, spring, devops, aws, java]
After removing consecutive dups: [java, spring, devops, aws, java]

Iterating Maps — entrySet Iterator

Map does not implement Iterable directly. To iterate a Map, use one of its collection views — and all three are Iterable.

1// File: MapIteratorDemo.java 2 3import java.util.HashMap; 4import java.util.Iterator; 5import java.util.Map; 6 7public class MapIteratorDemo { 8 9 public static void main(String[] args) { 10 11 Map<String, Integer> cityRank = new HashMap<>(); 12 cityRank.put("Mumbai", 1); 13 cityRank.put("Delhi", 2); 14 cityRank.put("Bengaluru", 3); 15 cityRank.put("Chennai", 4); 16 cityRank.put("Hyderabad", 5); 17 18 // entrySet() iterator — gives both key and value in one step 19 System.out.println("=== entrySet iterator ==="); 20 Iterator<Map.Entry<String, Integer>> entryIt = cityRank.entrySet().iterator(); 21 while (entryIt.hasNext()) { 22 Map.Entry<String, Integer> entry = entryIt.next(); 23 System.out.printf(" %-12s rank %d%n", entry.getKey(), entry.getValue()); 24 } 25 26 // Removing entries via entrySet iterator — the correct pattern 27 System.out.println("\n=== Remove entries with rank > 3 ==="); 28 Iterator<Map.Entry<String, Integer>> removalIt = cityRank.entrySet().iterator(); 29 while (removalIt.hasNext()) { 30 Map.Entry<String, Integer> entry = removalIt.next(); 31 if (entry.getValue() > 3) { 32 removalIt.remove(); // removes from the backing map 33 } 34 } 35 System.out.println("Remaining cities: " + cityRank); 36 } 37}
Output:
=== entrySet iterator ===
  Bengaluru    rank 3
  Mumbai       rank 1
  Delhi        rank 2
  Chennai      rank 4
  Hyderabad    rank 5

=== Remove entries with rank > 3 ===
Remaining cities: {Bengaluru=3, Mumbai=1, Delhi=2}

ListIterator — Bidirectional and In-Place Modification

ListIterator<E> extends Iterator and adds backward traversal, index awareness, set() for in-place replacement, and add() for mid-traversal insertion.

1// File: ListIteratorDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5import java.util.ListIterator; 6 7public class ListIteratorDemo { 8 9 public static void main(String[] args) { 10 11 List<String> queue = new ArrayList<>( 12 List.of("OrderA", "OrderB", "OrderC", "OrderD", "OrderE") 13 ); 14 15 // Forward traversal 16 System.out.println("=== Forward pass ==="); 17 ListIterator<String> lit = queue.listIterator(); 18 while (lit.hasNext()) { 19 System.out.printf(" [index %d] %s%n", lit.nextIndex(), lit.next()); 20 } 21 22 // Backward traversal — cursor is at the end after the forward pass 23 System.out.println("\n=== Backward pass (cursor at end) ==="); 24 while (lit.hasPrevious()) { 25 System.out.printf(" [index %d] %s%n", lit.previousIndex(), lit.previous()); 26 } 27 28 // set() — replace the last element returned by next() or previous() 29 System.out.println("\n=== In-place replacement with set() ==="); 30 ListIterator<String> setIt = queue.listIterator(); 31 while (setIt.hasNext()) { 32 String order = setIt.next(); 33 if ("OrderC".equals(order)) { 34 setIt.set("PRIORITY-OrderC"); // replaces without cursor change 35 } 36 } 37 System.out.println("After set(): " + queue); 38 39 // Start from a specific index — useful for mid-list operations 40 System.out.println("\n=== Iterator starting at index 2 ==="); 41 ListIterator<String> midIt = queue.listIterator(2); 42 while (midIt.hasNext()) { 43 System.out.println(" " + midIt.next()); 44 } 45 } 46}
Output:
=== Forward pass ===
  [index 0] OrderA
  [index 1] OrderB
  [index 2] OrderC
  [index 3] OrderD
  [index 4] OrderE

=== Backward pass (cursor at end) ===
  [index 4] OrderE
  [index 3] OrderD
  [index 2] OrderC
  [index 1] OrderB
  [index 0] OrderA

=== In-place replacement with set() ===
After set(): [OrderA, OrderB, PRIORITY-OrderC, OrderD, OrderE]

=== Iterator starting at index 2 ===
  PRIORITY-OrderC
  OrderD
  OrderE

Real-World Example — CRED Bill Payment Queue Processor

A bill payment processor at CRED processes a queue of pending bills. Some bills have already been paid through a different channel (duplicate payment), and some have expired. The processor must iterate the queue, skip clean bills, remove paid duplicates, and flag expired ones — all in a single pass without touching the list outside the iterator.

1// File: PendingBill.java 2 3public class PendingBill { 4 5 private final String billId; 6 private final String category; 7 private final double amount; 8 private final String status; // PENDING, PAID_ELSEWHERE, EXPIRED 9 10 public PendingBill(String billId, String category, double amount, String status) { 11 this.billId = billId; 12 this.category = category; 13 this.amount = amount; 14 this.status = status; 15 } 16 17 public String getBillId() { return billId; } 18 public String getCategory() { return category; } 19 public double getAmount() { return amount; } 20 public String getStatus() { return status; } 21 22 @Override 23 public String toString() { 24 return String.format("[%s] %-14s Rs.%7.2f %s", 25 billId, category, amount, status); 26 } 27}
1// File: BillQueueProcessor.java 2 3import java.util.ArrayList; 4import java.util.Iterator; 5import java.util.List; 6 7public class BillQueueProcessor { 8 9 private final List<PendingBill> billQueue = new ArrayList<>(); 10 11 public void enqueue(PendingBill bill) { 12 billQueue.add(bill); 13 } 14 15 // Single-pass cleanup: removes PAID_ELSEWHERE and EXPIRED bills 16 // Uses iterator.remove() — the only safe removal during traversal 17 public ProcessingReport cleanAndProcess() { 18 List<PendingBill> processed = new ArrayList<>(); 19 List<PendingBill> removed = new ArrayList<>(); 20 21 Iterator<PendingBill> it = billQueue.iterator(); 22 while (it.hasNext()) { 23 PendingBill bill = it.next(); 24 switch (bill.getStatus()) { 25 case "PENDING" -> { 26 processed.add(bill); // valid — keep in queue for payment 27 } 28 case "PAID_ELSEWHERE", "EXPIRED" -> { 29 removed.add(bill); 30 it.remove(); // safe removal — syncs modCount internally 31 } 32 } 33 } 34 return new ProcessingReport(processed, removed); 35 } 36 37 public void printQueue() { 38 System.out.println("=".repeat(56)); 39 System.out.println(" CRED BILL QUEUE (" + billQueue.size() + " items)"); 40 System.out.println("=".repeat(56)); 41 billQueue.forEach(bill -> System.out.println(" " + bill)); 42 System.out.println("=".repeat(56)); 43 } 44 45 record ProcessingReport(List<PendingBill> toProcess, List<PendingBill> removed) {} 46 47 public static void main(String[] args) { 48 49 BillQueueProcessor processor = new BillQueueProcessor(); 50 51 processor.enqueue(new PendingBill("B001", "Electricity", 1842.50, "PENDING")); 52 processor.enqueue(new PendingBill("B002", "Broadband", 999.00, "PAID_ELSEWHERE")); 53 processor.enqueue(new PendingBill("B003", "Mobile", 599.00, "PENDING")); 54 processor.enqueue(new PendingBill("B004", "Credit Card", 12500.00, "EXPIRED")); 55 processor.enqueue(new PendingBill("B005", "Gas", 450.00, "PENDING")); 56 processor.enqueue(new PendingBill("B006", "Insurance", 3200.00, "PAID_ELSEWHERE")); 57 processor.enqueue(new PendingBill("B007", "OTT Subs", 149.00, "PENDING")); 58 59 System.out.println("BEFORE CLEANUP:"); 60 processor.printQueue(); 61 62 ProcessingReport report = processor.cleanAndProcess(); 63 64 System.out.println("\nAFTER CLEANUP:"); 65 processor.printQueue(); 66 67 System.out.println("\n--- Removed bills ---"); 68 report.removed().forEach(b -> System.out.println(" REMOVED: " + b)); 69 70 System.out.println("\n--- Bills queued for payment ---"); 71 report.toProcess().forEach(b -> System.out.println(" PROCESS: " + b)); 72 73 double totalDue = report.toProcess().stream() 74 .mapToDouble(PendingBill::getAmount).sum(); 75 System.out.printf("%n Total amount due: Rs. %.2f%n", totalDue); 76 } 77}
Output:
BEFORE CLEANUP:
========================================================
  CRED BILL QUEUE  (7 items)
========================================================
  [B001] Electricity    Rs.  1842.50  PENDING
  [B002] Broadband      Rs.   999.00  PAID_ELSEWHERE
  [B003] Mobile         Rs.   599.00  PENDING
  [B004] Credit Card    Rs. 12500.00  EXPIRED
  [B005] Gas            Rs.   450.00  PENDING
  [B006] Insurance      Rs.  3200.00  PAID_ELSEWHERE
  [B007] OTT Subs       Rs.   149.00  PENDING
========================================================

AFTER CLEANUP:
========================================================
  CRED BILL QUEUE  (4 items)
========================================================
  [B001] Electricity    Rs.  1842.50  PENDING
  [B003] Mobile         Rs.   599.00  PENDING
  [B005] Gas            Rs.   450.00  PENDING
  [B007] OTT Subs       Rs.   149.00  PENDING
========================================================

--- Removed bills ---
  REMOVED: [B002] Broadband      Rs.   999.00  PAID_ELSEWHERE
  REMOVED: [B004] Credit Card    Rs. 12500.00  EXPIRED
  REMOVED: [B006] Insurance      Rs.  3200.00  PAID_ELSEWHERE

--- Bills queued for payment ---
  PROCESS: [B001] Electricity    Rs.  1842.50  PENDING
  PROCESS: [B003] Mobile         Rs.   599.00  PENDING
  PROCESS: [B005] Gas            Rs.   450.00  PENDING
  PROCESS: [B007] OTT Subs       Rs.   149.00  PENDING

  Total amount due: Rs. 3040.50

Performance Considerations

CollectionIterator typehasNext()next()remove()
ArrayListIndex-based (Itr)O(1)O(1)O(n) — shifts elements
LinkedListNode-basedO(1)O(1)O(1) — unlinks node
HashSetBucket scannerO(1) amortO(1) amortO(1) amort
TreeSetIn-order treeO(1) amortO(1) amortO(log n)
HashMap entrySetBucket scannerO(1) amortO(1) amortO(1) amort
CopyOnWriteArrayListSnapshot arrayO(1)O(1)Unsupported

Fail-fast vs fail-safe: Standard collection iterators (ArrayList, HashMap, HashSet) are fail-fast — they throw ConcurrentModificationException on structural modification outside the iterator. CopyOnWriteArrayList's iterator is fail-safe — it works on a snapshot taken at iterator creation and never throws CME, but it never sees modifications made after its creation either.

Memory: An Iterator object is lightweight — a few fields (cursor index or node pointer, lastRet, expectedModCount) with no copy of the collection data. Creating an iterator is O(1) and costs a few dozen bytes.

Best Practices

Use iterator.remove() for removal during traversal, or removeIf() for simple predicates. Never call list.remove() or list.add() inside a for-each loop. removeIf(predicate) is the cleaner Java 8+ approach when the removal condition is expressible as a lambda. Use explicit iterator.remove() only when the removal logic is more complex — for example, when it depends on the previous element seen or on accumulated state.

Never assume next() is safe without hasNext(). When a collection is modified between iterator creation and exhaustion — even by another method on the same thread — the size() you checked before the loop may no longer match the iterator's view. Always guard next() with hasNext() in a while loop, not a for loop with a cached size.

Use ListIterator for in-place replacement. When you need to update every element in a List during traversal — for example, normalising strings to lowercase or applying a discount to prices — ListIterator.set() is cleaner and safer than tracking the index manually and calling list.set(i, newValue) outside the iterator.

Prefer entrySet() iteration over keySet() plus get() for Map traversal. Iterating keySet() and calling map.get(key) inside the loop performs two hash lookups per entry. entrySet() gives both key and value in a single iterator step.

Common Mistakes

Mistake 1 — Calling list.remove() Inside for-each

1List<String> items = new ArrayList<>(List.of("A", "B", "C", "D", "E")); 2 3// WRONG — list.remove() increments modCount; iterator detects mismatch on next next() 4for (String item : items) { 5 if ("C".equals(item)) { 6 items.remove(item); // throws ConcurrentModificationException 7 } 8} 9 10// CORRECT — removeIf for simple conditions (Java 8+) 11items.removeIf("C"::equals); 12 13// CORRECT — explicit iterator for complex conditions 14Iterator<String> it = items.iterator(); 15while (it.hasNext()) { 16 if ("C".equals(it.next())) { 17 it.remove(); 18 } 19}

Mistake 2 — Calling remove() Before next()

1List<String> list = new ArrayList<>(List.of("X", "Y", "Z")); 2Iterator<String> it = list.iterator(); 3 4// WRONG — remove() requires a prior next() call to know WHAT to remove 5it.remove(); // throws IllegalStateException — lastRet is -1 6 7// CORRECT — always call next() before remove() 8it.next(); // returns "X", sets lastRet = 0 9it.remove(); // removes "X" — now safe

Mistake 3 — Calling next() on an Exhausted Iterator

1List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3)); 2Iterator<Integer> it = numbers.iterator(); 3 4it.next(); it.next(); it.next(); // consumed all three elements 5 6// WRONG — calling next() after hasNext() returns false 7int fourth = it.next(); // throws NoSuchElementException 8 9// CORRECT — always guard with hasNext() 10if (it.hasNext()) { 11 int value = it.next(); 12}

Mistake 4 — Sharing an Iterator Across Threads Without Synchronisation

1// WRONG — two threads sharing one iterator causes race conditions and CME 2Iterator<String> sharedIt = myList.iterator(); 3 4// Thread 1 and Thread 2 both call sharedIt.next() concurrently 5// The cursor is not volatile — one thread may skip elements or throw CME 6 7// CORRECT — each thread gets its own iterator 8// Thread 1: 9Iterator<String> it1 = myList.iterator(); 10// Thread 2: 11Iterator<String> it2 = myList.iterator(); 12 13// OR for thread-safe iteration on a shared list, use CopyOnWriteArrayList 14// whose iterator works on a snapshot and never throws CME

Interview Questions

Q1. What is the Iterator interface and what methods does it define?

java.util.Iterator<E> is an interface in java.util that provides a cursor for traversing any Java collection one element at a time. It defines three methods: hasNext() returns true if more elements remain; next() returns the current element and advances the cursor; remove() removes the last element returned by next() — the only structurally safe way to remove during traversal. ListIterator extends Iterator with bidirectional traversal, index queries, set(), and add() for List types specifically.

Q2. What is ConcurrentModificationException and why does Iterator throw it?

Standard Java collection iterators are fail-fast. Each iterator snapshots the collection's modCount (an internal counter incremented on every structural change) at creation time. On every next() call, the iterator checks whether modCount still matches its snapshot. If the collection was structurally modified outside the iterator — by calling list.add() or list.remove() directly — modCount changes and the next next() call throws ConcurrentModificationException. This is a programming error detection mechanism. Fix it by using iterator.remove() or list.removeIf() instead of direct modification.

Q3. Why is iterator.remove() safe when list.remove() inside for-each is not?

When you call iterator.remove(), the iterator first performs the actual removal on the backing collection (incrementing modCount), then updates its own expectedModCount to match the new modCount. The iterator accounts for its own structural change. When you call list.remove() directly, modCount increments but the iterator's expectedModCount is never updated. The next next() call detects the mismatch and throws. removeIf() is safe for the same reason — it handles its own modCount bookkeeping internally.

Q4. What is the difference between Iterator and ListIterator?

Iterator is the base traversal interface — forward-only, works on any Collection, three methods: hasNext(), next(), remove(). ListIterator extends Iterator and is specific to List — it adds: backward traversal with hasPrevious() and previous(); index awareness with nextIndex() and previousIndex(); in-place replacement with set(element) replaces the last returned element; mid-traversal insertion with add(element) inserts before the next-to-be-returned element. ListIterator also allows constructing it at a specific starting index via list.listIterator(fromIndex).

Q5. What is the difference between fail-fast and fail-safe iterators in Java?

Fail-fast iterators — used by ArrayList, HashMap, HashSet, TreeSet — throw ConcurrentModificationException if the collection is structurally modified while iteration is in progress. They detect this via a modCount field comparison. Fail-safe iterators — used by CopyOnWriteArrayList and ConcurrentHashMap — do not throw. CopyOnWriteArrayList creates a complete copy of the backing array when iteration starts; the iterator traverses the snapshot and never sees subsequent modifications. ConcurrentHashMap uses weakly consistent iteration — it may or may not reflect modifications made during traversal, but it never throws.

Q6. Can you remove elements from a Set using its Iterator?

Yes. HashSet, LinkedHashSet, and TreeSet all support iterator.remove(). The pattern is the same as for ArrayList: get the iterator, call next(), then call iterator.remove() if the element should be removed. Calling set.remove(element) inside a for-each loop over the same set throws ConcurrentModificationException. For sets, removeIf() is often the cleanest approach: mySet.removeIf(item -> someCondition(item)).

FAQs

What is the difference between Iterator and Enumeration in Java?

Enumeration<E> is a pre-Java-2 traversal interface with hasMoreElements() and nextElement(). It predates the Collections Framework and is used by legacy classes like Vector and Hashtable. Iterator replaced it in Java 1.2 — it adds remove() and uses shorter, more consistent method names. Enumeration has no remove() method and does not detect concurrent modification. All new code should use Iterator. The only reason to use Enumeration today is when calling legacy APIs that return it.

Does Iterator copy the collection's data?

No. An Iterator is a lightweight cursor object — for ArrayList it is an int index into the existing backing array; for LinkedList it is a Node reference. No data is copied on iterator creation. Only CopyOnWriteArrayList creates a full array snapshot for its iterator, to ensure the iterator is isolated from concurrent writes. For all other standard collections, the iterator works directly against the live data.

Can two for-each loops run concurrently on the same ArrayList?

In a single-threaded program, two consecutive for-each loops work fine — each gets a fresh iterator. Two simultaneous for-each loops on the same list from different threads can cause ConcurrentModificationException if either thread modifies the list, or may produce incorrect results due to non-atomic reads even without modification. For concurrent read iteration, use CopyOnWriteArrayList, or synchronise the block externally with Collections.synchronizedList().

What happens if I call next() on an Iterator after the collection is cleared?

If you call list.clear() outside the iterator — directly on the collection — the iterator will throw ConcurrentModificationException on the next next() call. clear() increments modCount, and the iterator detects the mismatch. If you want to clear a collection while iterating it, call iterator.remove() on each element during traversal, or call list.clear() after the loop ends.

Is there any way to iterate backwards over a Set using Iterator?

No — Set has no concept of order for HashSet and no ListIterator equivalent. TreeSet iterates in ascending sorted order via its regular iterator. To iterate TreeSet in descending order, use treeSet.descendingSet().iterator() — this returns a NavigableSet view with reversed ordering, and iterating it gives elements in descending order.

What is the remove() method's behaviour when the underlying implementation does not support it?

Iterator.remove() is a default method that throws UnsupportedOperationException by default. Implementations returned by unmodifiable collections — Collections.unmodifiableList(), List.of() — throw UnsupportedOperationException if you call remove(). This matches the contract: the collection cannot be modified, so neither its iterator nor the iterator's remove() supports modification. Always check the collection's modifiability before calling iterator.remove() in generic utility methods.

Summary

Iterator<E> is the cursor interface that drives every collection traversal in Java. Its three methods — hasNext(), next(), remove() — are deceptively simple. The depth is in the fail-fast mechanism: the modCount field tracks structural changes, and any modification outside the iterator invalidates the cursor with a ConcurrentModificationException. iterator.remove() is safe because it updates expectedModCount after performing the removal — the iterator accounts for changes it makes itself.

For most read-only traversal, for-each is cleaner. The explicit Iterator earns its place when removal during traversal is needed, or when the same cursor must be passed across method boundaries. ListIterator adds backward traversal and in-place set() for List-specific use cases.

For interviews: know the difference between fail-fast and fail-safe, explain why iterator.remove() is safe but list.remove() inside for-each is not, and describe the modCount mechanism. These questions come up at every level — from fresher TCS rounds to senior Razorpay discussions on concurrent data structures.

What to Read Next