Java design pattern deep dive
Iterator Pattern in Java: 100 interview questions with professional answers.
Learn how the Iterator pattern lets you traverse a collection's elements without exposing its internal structure, how java.util.Iterator and Iterable really work under the hood, why fail-fast and fail-safe iteration are deliberate trade-offs, and how Spliterator and Streams generalize traversal for lazy and parallel processing.
What makes a good Iterator answer?
Interviewers want to see that you understand traversal as a first-class responsibility, not an afterthought bolted onto a collection: where the cursor state lives, what happens under concurrent mutation, and when a simpler mechanism beats a hand-rolled iterator.
| Approach | Use when | Watch out for |
|---|---|---|
| Iterator / Iterable | You need explicit, client-driven control over the traversal, including the ability to pause, resume, or remove elements mid-walk. | Boilerplate cursor state to manage correctly, and a fail-fast contract that is only best-effort. |
| Enhanced for-each loop | You just want to read every element in order with no need to remove elements or track an index. | No access to the underlying Iterator, so you cannot call remove() or break out with custom logic mid-loop cleanly. |
| Enumeration (legacy) | You are working with pre-Collections-framework classes such as Vector or Hashtable that still expose it. | No remove() method at all, and it predates generics-friendly conventions; wrap it in an Iterator adapter for new code. |
| Spliterator / Streams | You want lazy, potentially parallel processing, or you are composing map/filter/reduce operations declaratively. | Streams are single-use and harder to pause or resume manually; parallel splitting needs a balanced, sized source to pay off. |
Topics
Interview questions and answers
Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.
1. Explain the Iterator design pattern in Java: its intent, the four GoF roles involved, and the real problem it solves when a client needs to traverse a collection.
The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. The GoF structure has four roles: an Iterator interface declaring traversal operations such as hasNext()/next(); a ConcreteIterator that implements those operations and tracks the current position; an Aggregate (or Iterable) interface declaring a factory method that returns an iterator; and a ConcreteAggregate that holds the actual elements and implements that factory method.
It solves a concrete problem: without it, every piece of client code that wants to walk a collection needs to know whether it is an array, a linked list, or a tree, and how to step through that specific structure. The pattern decouples "how to walk" from "what is being walked," so the same client loop works unchanged against any aggregate that exposes an Iterator.
interface Aggregate<T> {
Iterator<T> createIterator();
}
class NameCollection implements Aggregate<String> {
private final String[] names;
NameCollection(String[] names) { this.names = names; }
@Override
public Iterator<String> createIterator() {
return new NameIterator();
}
private class NameIterator implements Iterator<String> {
private int index = 0;
public boolean hasNext() { return index < names.length; }
public String next() { return names[index++]; }
}
}
2. Why should the traversal algorithm live in a separate Iterator object rather than being implemented as methods directly on the collection class itself?
If a collection exposes traversal only through its own methods, such as a bespoke getNext(int cursor), it can only ever support one traversal strategy and one in-progress traversal at a time unless it starts juggling multiple cursor fields internally. Extracting the algorithm into a separate iterator object gives each traversal its own independent state, and lets the same aggregate support multiple simultaneous, differently-ordered traversal strategies, such as forward, reverse, or filtered iterators, without polluting the collection class with traversal-specific fields.
This is also a direct application of the single responsibility principle: the aggregate's job is to own and manage its elements, while the iterator's job is to know how to walk them in a particular order. Changing the traversal order later, for example switching a tree from in-order to breadth-first, means writing a new ConcreteIterator without touching the aggregate at all.
3. Implement a custom singly linked list in Java that supports the enhanced for-each loop by implementing Iterable, including the internal ConcreteIterator class.
Make the list class implement Iterable<T> and return a private inner iterator class from iterator(). The inner iterator simply walks the linked Node chain, advancing a current reference on each call to next(), which is exactly what the enhanced for-loop needs to work.
class SimpleLinkedList<T> implements Iterable<T> {
private Node<T> head;
private static class Node<T> {
T value; Node<T> next;
Node(T value) { this.value = value; }
}
void add(T value) {
Node<T> node = new Node<>(value);
if (head == null) { head = node; return; }
Node<T> cur = head;
while (cur.next != null) cur = cur.next;
cur.next = node;
}
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private Node<T> current = head;
public boolean hasNext() { return current != null; }
public T next() {
if (current == null) throw new NoSuchElementException();
T value = current.value;
current = current.next;
return value;
}
};
}
}
// usage
for (String name : simpleLinkedList) { System.out.println(name); }
4. Implement an Iterable binary tree in Java that performs an in-order traversal, and explain how you would support depth-first traversal without recursion inside the iterator's next() method.
Recursion inside next() does not work well because the iterator must pause between calls and resume exactly where it left off; a recursive in-order walk would need to run to completion in one call. Instead, maintain an explicit stack that simulates the recursive call stack: push left children as you descend, and when you pop a node, push its right subtree before continuing.
class TreeNode<T> { T value; TreeNode<T> left, right; }
class InOrderIterator<T> implements Iterator<T> {
private final Deque<TreeNode<T>> stack = new ArrayDeque<>();
InOrderIterator(TreeNode<T> root) { pushLeftSpine(root); }
private void pushLeftSpine(TreeNode<T> node) {
while (node != null) { stack.push(node); node = node.left; }
}
public boolean hasNext() { return !stack.isEmpty(); }
public T next() {
if (stack.isEmpty()) throw new NoSuchElementException();
TreeNode<T> node = stack.pop();
pushLeftSpine(node.right);
return node.value;
}
}
5. Design an Iterable wrapper around a paginated REST API so that client code can iterate over every result across all pages using a plain enhanced for-each loop.
The iterator holds a buffer of the current page's results plus a cursor into that buffer, and the underlying page's "next page token." When hasNext() finds the buffer exhausted but a next-page token still exists, it fetches the next page eagerly inside hasNext() so that next() can remain a simple, side-effect-free read.
class PagedResultIterable<T> implements Iterable<T> {
private final PagedApiClient<T> client;
PagedResultIterable(PagedApiClient<T> client) { this.client = client; }
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private List<T> buffer = Collections.emptyList();
private int index = 0;
private String nextPageToken = null;
private boolean firstPageLoaded = false;
public boolean hasNext() {
if (index < buffer.size()) return true;
if (firstPageLoaded && nextPageToken == null) return false;
Page<T> page = client.fetchPage(nextPageToken);
buffer = page.items(); index = 0; nextPageToken = page.nextToken(); firstPageLoaded = true;
return !buffer.isEmpty();
}
public T next() {
if (!hasNext()) throw new NoSuchElementException();
return buffer.get(index++);
}
};
}
}
6. Describe the exact contract of java.util.Iterator's hasNext(), next(), and remove() methods, including what each is guaranteed and not guaranteed to do.
hasNext() returns true if the iteration has more elements; it must be safe to call repeatedly with no side effects and no state change. next() returns the next element and advances the cursor, throwing NoSuchElementException if no more elements remain, meaning callers are expected to check hasNext() first. remove() removes from the underlying collection the last element returned by next(), and may be called only once per call to next(); calling it before any next() call, or twice in a row, throws IllegalStateException.
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String value = it.next();
if (value.isEmpty()) {
it.remove(); // legal: exactly one call after next()
}
}
7. Why is Iterator.remove() an optional operation in the interface contract, and what does throwing UnsupportedOperationException signal to callers?
remove() is optional because not every aggregate supports safe or meaningful removal during traversal: an iterator over an immutable list, a fixed-size array-backed view, or a read-only snapshot has no sensible way to remove an element. Rather than forcing every implementer to support mutation, the interface declares a default body that throws UnsupportedOperationException, and implementations that do support removal override it.
UnsupportedOperationException is a clear, explicit signal to the caller: "this particular iterator's source does not support structural removal," which is far better than silently no-op'ing or corrupting internal state. Defensive code that might iterate over either a mutable or immutable source should catch this exception, or check the source's documented mutability up front, rather than assuming remove() always works.
8. Implement bidirectional traversal for a custom list-backed collection using ListIterator, and explain what additional guarantees ListIterator provides over a plain Iterator.
ListIterator extends Iterator with hasPrevious()/previous() for backward movement, nextIndex()/previousIndex() to report position, and set()/add() to replace or insert elements during traversal, none of which plain Iterator offers. It is only meaningful for list-like, ordered aggregates, since "previous" and "index" require a defined sequence.
class ArrayBackedList<T> {
private Object[] data; private int size;
ListIterator<T> listIterator() {
return new ListIterator<T>() {
private int cursor = 0;
public boolean hasNext() { return cursor < size; }
public boolean hasPrevious() { return cursor > 0; }
@SuppressWarnings("unchecked")
public T next() { return (T) data[cursor++]; }
@SuppressWarnings("unchecked")
public T previous() { return (T) data[--cursor]; }
public int nextIndex() { return cursor; }
public int previousIndex() { return cursor - 1; }
public void set(T t) { data[cursor - 1] = t; }
public void add(T t) { /* insert-at-cursor logic */ }
public void remove() { /* shift-left removal logic */ }
};
}
}
9. Explain why hasNext() must be idempotent and free of externally visible side effects, and describe what breaks if a custom iterator's hasNext() implementation accidentally advances state.
Callers are entitled to call hasNext() any number of times without affecting the outcome of the next next() call, and many idioms, including the enhanced for-loop and defensive "peek before consume" code, rely on that guarantee. If hasNext() accidentally advances an internal cursor or consumes a buffered element as a side effect, calling it twice in a row (which is entirely legal per the contract) will silently skip an element.
This bug is most common in iterators wrapping a stream or socket, where "checking if more data is available" is implemented by eagerly reading ahead. The fix is to buffer the read-ahead value in a field and return it from the next next() call instead of discarding it, so repeated hasNext() calls remain safe.
hasNext() is implemented by "try to fetch one more element and see if it succeeds" must cache the fetched element rather than dropping it.10. Explain the contract around NoSuchElementException: when exactly must a correct Iterator implementation throw it, and how would you verify this in a unit test?
A conforming Iterator must throw NoSuchElementException from next() when it is called after the iteration is already exhausted, that is, after hasNext() would return false. It must not throw it prematurely while elements remain, and it must not instead return null or some sentinel value, since that would silently corrupt calling code that expects the standard contract.
@Test
void throwsAfterExhaustingAllElements() {
Iterator<String> it = List.of("a", "b").iterator();
it.next(); it.next();
assertThrows(NoSuchElementException.class, it::next);
}
11. Explain, with a concrete before-and-after example, how the Iterator pattern lets a client traverse a collection without ever knowing whether it is backed by an array, a linked list, or a tree.
Without the pattern, client code that needs to walk a collection would call structure-specific accessors, such as indexing into an array or following a next field on a node, meaning the client's code changes if the aggregate's internal representation later changes from an array to a linked structure. With the pattern, the client only ever calls hasNext()/next() against the Iterator interface, so the aggregate is free to switch its internal storage entirely without breaking a single line of client code.
// before: client code coupled to array internals
for (int i = 0; i < collection.size(); i++) {
process(collection.elementAt(i));
}
// after: client code coupled only to Iterator
for (Item item : collection) { // collection.iterator() under the hood
process(item);
}
12. Explain why two independent iterators obtained from the same aggregate must not interfere with each other's traversal state, and show how a naive implementation could accidentally violate this.
If two callers each call iterator() on the same collection and walk it concurrently, or in an interleaved fashion, each expects its own private progress through the elements. This only works if the cursor position lives on the ConcreteIterator instance, not on the aggregate itself; storing the cursor as a shared field on the aggregate would make one iterator's next() call silently affect the other iterator's position.
// broken: cursor lives on the aggregate, shared across all iterators
class BrokenAggregate {
private int sharedCursor = 0; // wrong location for cursor state
Iterator<String> iterator() {
return new Iterator<String>() {
public boolean hasNext() { return sharedCursor < data.length; }
public String next() { return data[sharedCursor++]; } // corrupts other iterators
};
}
}
The fix is simply to declare the cursor as a field of the returned iterator instance rather than the enclosing aggregate, so each call to iterator() produces an object with fresh, independent state.
13. Compare the classic GoF Iterator pattern's roles to Java's java.util.Iterator and Iterable interfaces, and point out where the JDK's naming and structure diverge slightly from the textbook pattern.
The mapping is close but not identical: GoF's Iterator maps directly to java.util.Iterator, and GoF's Aggregate maps to java.lang.Iterable, whose single method is named iterator() rather than the textbook's generic createIterator(). The JDK also adds a remove() method directly on Iterator, letting an iterator mutate its source during traversal, a capability the original GoF pattern description does not emphasize.
Java 8 further extended Iterator with a default forEachRemaining() method and extended Iterable with a default forEach() method, both supporting internal iteration, which is a capability layered on top of, not part of, the original external-iteration-focused GoF design.
14. Explain why Java splits traversal into two separate interfaces, Iterable and Iterator, instead of having a collection implement iteration directly, and what problem this split solves.
Iterable's single method, iterator(), acts as a factory that produces a fresh Iterator instance on every call. This split exists precisely so that a collection can be walked multiple times, and multiple times simultaneously, since each call to iterator() hands out independent cursor state, whereas the collection object itself does not carry any single, shared "current position."
List<Integer> numbers = List.of(1, 2, 3);
Iterator<Integer> first = numbers.iterator();
Iterator<Integer> second = numbers.iterator();
first.next(); // advances only "first", "second" is unaffected
15. How would you implement hasNext() and next() for an iterator over a data source, such as a socket or a growing log file, where the total number of elements is not known in advance?
When there is no upfront size, hasNext() must actively probe the source, for example attempting a non-blocking read or checking for an end-of-stream marker, rather than comparing against a known count. The safest pattern is to eagerly read one element ahead inside hasNext(), cache it in a field, and return it from the following next() call, so repeated hasNext() calls remain side-effect free per the contract discussed earlier.
class SocketLineIterator implements Iterator<String> {
private final BufferedReader reader;
private String bufferedLine;
private boolean exhausted = false;
SocketLineIterator(BufferedReader reader) { this.reader = reader; }
public boolean hasNext() {
if (bufferedLine != null) return true;
if (exhausted) return false;
try {
bufferedLine = reader.readLine();
if (bufferedLine == null) exhausted = true;
} catch (IOException e) { throw new UncheckedIOException(e); }
return bufferedLine != null;
}
public String next() {
if (!hasNext()) throw new NoSuchElementException();
String line = bufferedLine;
bufferedLine = null;
return line;
}
}
16. What is a fail-fast iterator, and what specific event causes java.util.ConcurrentModificationException to be thrown during iteration over a collection like ArrayList?
A fail-fast iterator detects that its backing collection has been structurally modified, meaning elements were added or removed, after the iterator was created, and throws ConcurrentModificationException as soon as it notices, rather than continuing with potentially corrupted or inconsistent state. Structural modification includes direct calls to the collection's own add()/remove() made outside the iterator, not modifications made through the iterator's own remove() method.
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) {
list.remove(s); // structural modification outside the iterator
}
} // throws ConcurrentModificationException on the next hasNext()/next()
17. Explain how ArrayList's internal modCount field implements fail-fast detection, including exactly what the iterator checks on each call to next().
ArrayList keeps a private modCount field that is incremented every time the list is structurally modified. When iterator() is called, the returned iterator snapshots that value into its own expectedModCount field. On every subsequent call to next() (and remove()), the iterator compares the list's live modCount against its cached expectedModCount, and throws ConcurrentModificationException if they no longer match.
// simplified excerpt of java.util.ArrayList.Itr
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
Calling the iterator's own remove() works safely because it also updates expectedModCount to match the list's new modCount after performing the removal, keeping the two values in sync.
18. Which commonly used JDK collections are fail-fast by default, and are there any that deliberately do not provide this guarantee at all?
ArrayList, LinkedList, HashMap, HashSet, TreeMap, and TreeSet, essentially all of the "core," non-concurrent collections framework classes, are fail-fast. The concurrent collections in java.util.concurrent, such as ConcurrentHashMap and CopyOnWriteArrayList, deliberately opt out of fail-fast behavior in favor of weakly consistent or snapshot iteration instead, since throwing on concurrent access would defeat their purpose in multi-threaded code.
19. Explain how CopyOnWriteArrayList achieves fail-safe iteration, including what happens to an in-progress iterator when another thread adds an element concurrently.
CopyOnWriteArrayList stores its elements in an internal array reference, and every mutating operation, such as add() or remove(), creates an entirely new array with the change applied and atomically swaps the reference, leaving the old array untouched. An iterator obtained before the mutation holds a reference to the old array snapshot and simply keeps iterating over it, completely unaware that a new array now exists.
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(List.of("a", "b"));
Iterator<String> it = list.iterator(); // snapshots the current array
list.add("c"); // creates a new array; "it" still sees only ["a", "b"]
while (it.hasNext()) System.out.println(it.next()); // prints a, b — never throws CME
20. Explain what "weakly consistent" means for ConcurrentHashMap's iterators, and how this differs from both fail-fast behavior and CopyOnWriteArrayList's full-snapshot behavior.
ConcurrentHashMap's iterators are weakly consistent: they are guaranteed not to throw ConcurrentModificationException, guaranteed to traverse elements that existed both before and after iteration began, and guaranteed to reflect some, but not necessarily all, modifications made during the traversal itself. This differs from fail-fast collections, which throw rather than tolerate concurrent structural change, and from CopyOnWriteArrayList, which freezes a complete, unchanging snapshot at iterator-creation time and therefore never reflects any concurrent modification at all.
In practice this means a ConcurrentHashMap iterator might or might not see an entry added by another thread mid-traversal, but it will never crash and will never see a torn or corrupted internal bucket structure.
21. Discuss the trade-off between fail-fast and fail-safe iteration in terms of staleness versus safety, and how you would decide which to use for a given production workload.
Fail-fast iteration favors correctness signaling over availability: it would rather crash loudly the moment it detects a race than silently produce a possibly-inconsistent result, which is the right default for single-threaded code where a ConcurrentModificationException almost always indicates a genuine bug. Fail-safe iteration favors availability over freshness: it guarantees the traversal will complete without throwing, at the cost of potentially iterating over stale or partially-updated data.
Choose fail-safe collections such as ConcurrentHashMap or CopyOnWriteArrayList specifically when multiple threads genuinely need to read and write the same collection concurrently and an occasional stale read is acceptable, such as a read-heavy configuration cache. For anything single-threaded, or where staleness is not tolerable, prefer a fail-fast collection plus proper external synchronization instead.
| Property | Fail-fast (ArrayList, HashMap) | Fail-safe (CopyOnWriteArrayList, ConcurrentHashMap) |
|---|---|---|
| On concurrent structural change | Throws ConcurrentModificationException | Completes without throwing |
| Data freshness | Always current up to the point of the throw | May be stale or partially updated |
| Write cost | Cheap, in-place mutation | Often expensive (array copy or bucket-level locking) |
22. Explain the exact mechanism by which removing an element from a List using the collection's own remove() method inside a for-each loop causes a ConcurrentModificationException.
The enhanced for-loop compiles down to an explicit Iterator obtained once before the loop begins, whose expectedModCount is fixed at that moment. Calling list.remove(element) directly bypasses the iterator entirely, incrementing the list's live modCount without updating the iterator's cached copy, so the very next call the loop makes to hasNext() or next() detects the mismatch and throws.
A subtle trap is that removing the second-to-last element can sometimes appear to work without throwing, because hasNext() happens to return false before the mismatch is ever checked, which misleads developers into thinking the pattern is safe when it is actually just a lucky edge case.
23. Show the correct way to remove elements from a List while iterating over it, and explain why this approach avoids ConcurrentModificationException.
Obtain the Iterator explicitly and call its own remove() method instead of the collection's remove(). The iterator's remove() performs the structural change and then synchronizes its cached expectedModCount with the list's updated modCount in the same operation, so no mismatch is ever detected.
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String value = it.next();
if (value.isEmpty()) {
it.remove(); // safe: iterator stays in sync with the list
}
}
// alternative for simple predicate-based removal
list.removeIf(String::isEmpty);
24. Why does the Javadoc for ConcurrentModificationException explicitly describe fail-fast behavior as "best-effort" rather than a guaranteed contract, and what does this mean for code that relies on it?
Detecting every possible concurrent modification with perfect reliability would require synchronizing every read and write, which would defeat the performance goals of non-thread-safe collections like ArrayList. Instead, the JDK implements a cheap heuristic, the modCount comparison, that catches the overwhelming majority of real bugs without adding synchronization overhead to the common single-threaded case, but it cannot catch every possible race, especially under concurrent access from multiple threads where the check itself is not atomic.
This means code must never rely on ConcurrentModificationException as a correctness mechanism, such as using it to detect application-level concurrent access for locking purposes; it is a debugging aid for catching accidental single-threaded bugs, not a safety net for genuinely concurrent code.
25. Implement your own custom fail-fast iterator for a hand-rolled collection class, including a modCount-style field and the comodification check.
Add a private counter field to the aggregate that increments on every structural mutation, then have the iterator capture that counter's value at creation time and re-check it before every next() call, mirroring exactly how ArrayList does it internally.
class SimpleBag<T> implements Iterable<T> {
private final List<T> items = new ArrayList<>();
private int modCount = 0;
void add(T item) { items.add(item); modCount++; }
void removeFirst() { if (!items.isEmpty()) { items.remove(0); modCount++; } }
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private final int expectedModCount = modCount;
private int index = 0;
public boolean hasNext() { return index < items.size(); }
public T next() {
if (modCount != expectedModCount) throw new ConcurrentModificationException();
if (!hasNext()) throw new NoSuchElementException();
return items.get(index++);
}
};
}
}
26. Discuss the performance trade-off of CopyOnWriteArrayList for a workload that iterates frequently but writes rarely, versus one that writes frequently and iterates rarely.
CopyOnWriteArrayList is a strong fit for iteration-heavy, write-rare workloads such as a list of registered event listeners: reads never block and never throw, and the rare write's O(n) array-copy cost is easily amortized across many cheap reads. It is a poor fit for write-heavy workloads, since every single add(), remove(), or set() call copies the entire backing array regardless of how small the change is, turning what should be an O(1) operation into O(n) and creating significant garbage-collection pressure under high write volume.
27. Design a custom snapshot iterator that copies a collection's elements at iterator-creation time, similar in spirit to CopyOnWriteArrayList's traversal guarantee, without paying the copy-on-every-write cost.
Instead of copying the backing array on every mutation, copy the elements once, only when iterator() itself is called, and hand the caller an iterator over that private copy. Writers continue to mutate the live collection directly and cheaply; only the (presumably less frequent) act of starting a traversal pays a copying cost, and that cost is proportional to the number of times iteration is started, not the number of writes.
class SnapshotIterableList<T> {
private final List<T> live = new ArrayList<>();
synchronized void add(T item) { live.add(item); } // cheap, no copy
synchronized Iterator<T> snapshotIterator() {
return List.copyOf(live).iterator(); // copy happens only here
}
}
28. Clarify the terminology confusion between "fail-safe" and "weakly consistent," since these terms are often used interchangeably but describe slightly different guarantees.
"Fail-safe" is the informal, commonly used umbrella term for any iterator that does not throw ConcurrentModificationException. "Weakly consistent," the term the JDK's own Javadoc actually uses for ConcurrentHashMap and similar classes, is a more precise guarantee: no exception is thrown, the iterator will not skip elements that were present for the entire iteration, and it may or may not reflect modifications made during iteration.
CopyOnWriteArrayList's iterator is fail-safe but arguably stronger than "weakly consistent," since it iterates over a fully immutable snapshot rather than a live, possibly-changing structure; some sources describe it separately as providing "snapshot" semantics rather than merely weak consistency.
29. Is it safe to call Map.Entry.setValue() on entries obtained from iterating a HashMap's entrySet, while the map itself is fail-fast against structural modification?
Yes: calling setValue() on an entry only changes the value associated with an existing key and is not a structural modification, meaning it does not add or remove a mapping and does not touch modCount. Fail-fast detection only guards against structural changes, such as adding or removing keys, made outside the iterator, not against updating the value of a key that already exists.
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.setValue(entry.getValue() * 2); // safe: not a structural change
}
// but map.put("newKey", 1) inside the same loop would still throw
30. Describe a bug where nested loops iterating the same ArrayList twice triggered a ConcurrentModificationException, and explain the underlying cause.
A common variant of this bug is an outer for-each loop over a list, with an inner block that removes matching elements from that same list using the list's own remove() rather than an iterator's remove(), for example while deduplicating or cross-referencing entries against themselves. Even though the removal happens logically "at the same nesting level" as the outer traversal, it still structurally modifies the list the outer iterator is walking, and the very next step of the outer loop throws.
for (String outer : list) {
for (String inner : new ArrayList<>(list)) { // safe: iterating a copy
if (outer.equals(inner)) {
list.remove(inner); // still mutates the list the OUTER loop is iterating
}
}
} // outer loop throws ConcurrentModificationException on its next step
The fix is to collect elements to remove into a separate list during the nested traversal, then remove them all after both loops complete, or use removeIf() with a predicate that captures the comparison logic directly.
31. Show what the enhanced for-each loop actually desugars into at the bytecode/source level, and explain why this proves it is syntactic sugar over Iterable and Iterator.
The compiler rewrites a for-each loop over any Iterable into an explicit call to iterator(), followed by a while loop that calls hasNext() and next(). No new bytecode instructions exist for the for-each loop itself; it is purely a compile-time transformation, which is exactly why any class implementing Iterable, including your own custom classes, automatically works with the for-each syntax.
// what you write
for (String name : names) {
System.out.println(name);
}
// what the compiler generates
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
String name = it.next();
System.out.println(name);
}
32. Arrays do not implement Iterable in Java, so explain how the enhanced for-each loop is still able to iterate over a plain array like int[] or String[].
The compiler special-cases arrays: when the for-each loop's target expression has an array type rather than an Iterable type, it desugars into an ordinary index-based counting loop instead of an Iterator-based one, bounded by the array's length field. This is purely a language-level accommodation, since retrofitting arrays to implement Iterable would have required boxing every primitive element.
// what you write
for (int n : numbers) { System.out.println(n); }
// what the compiler generates for an array (no Iterator involved)
for (int i = 0; i < numbers.length; i++) {
int n = numbers[i];
System.out.println(n);
}
33. Explain the Spliterator interface introduced in Java 8, including the purpose of tryAdvance(), trySplit(), and estimateSize(), and how it differs conceptually from Iterator.
Spliterator ("splittable iterator") generalizes Iterator to support both sequential and parallel traversal. tryAdvance(action) is the sequential-processing analogue of next(), performing the given action on the next element if one exists and returning whether it did. trySplit() attempts to partition the remaining elements into two spliterators so each half can be processed on a different thread, returning null when the source can no longer usefully be divided. estimateSize() reports an estimate of remaining elements, which the framework uses to decide whether further splitting is worthwhile.
Spliterator<String> spliterator = list.spliterator();
Spliterator<String> secondHalf = spliterator.trySplit(); // may return null
spliterator.forEachRemaining(System.out::println);
34. Explain how the Streams API, built on Spliterator, generalizes traditional Iterator-based iteration to support lazy evaluation and parallel processing.
A Stream pipeline is built from a source's Spliterator plus a chain of intermediate operations, such as map and filter, that are not actually executed until a terminal operation, such as collect or forEach, is invoked. This laziness lets the runtime fuse multiple operations into a single pass over the elements instead of materializing an intermediate collection after every step, something a hand-written loop of successive Iterator passes would not do automatically.
Parallel streams split the source's Spliterator recursively using trySplit(), dispatch each chunk to the common ForkJoinPool, and merge partial results, all invisibly to the caller, whereas a plain Iterator has no notion of splitting a traversal across threads at all.
35. Compare external iteration, where the client explicitly drives next() calls, versus internal iteration, where the aggregate drives a callback such as forEach(), and discuss the trade-offs of each.
External iteration puts the client in control: it decides exactly when to call next(), can pause the traversal indefinitely, break out early with a plain break, or interleave iteration with unrelated work. Internal iteration inverts control: the aggregate (or the Stream pipeline) drives the traversal and pushes each element into a supplied callback, which is more concise and lets the aggregate optimize the traversal strategy itself, including choosing to parallelize it, without the client's involvement.
// external iteration: client controls the loop
Iterator<Order> it = orders.iterator();
while (it.hasNext()) {
Order order = it.next();
if (order.isUrgent()) break; // easy early exit
}
// internal iteration: the aggregate controls the loop
orders.forEach(order -> process(order)); // no natural early-exit
| Aspect | External iteration (Iterator) | Internal iteration (forEach/Stream) |
|---|---|---|
| Early exit | Trivial with break/return | Awkward; needs exceptions or a short-circuiting terminal op |
| Conciseness | More boilerplate | More concise, declarative |
| Parallelism | Manual, error-prone | Built-in via parallel streams |
36. Since Streams favor internal iteration and lack a native break statement, how would you implement an early exit from a stream pipeline once a matching element is found?
Short-circuiting terminal operations such as findFirst(), anyMatch(), and limit() are the idiomatic way to stop processing early, since the Streams framework internally knows to stop pulling further elements once the condition is satisfied. For more complex early-exit logic, wrapping the loop back into external Iterator-based code, or throwing and catching a private unchecked exception (a discouraged but sometimes-used escape hatch), are the remaining options.
Optional<Order> firstUrgent = orders.stream()
.filter(Order::isUrgent)
.findFirst(); // short-circuits; does not process the rest of the stream
37. Implement a custom Spliterator for a domain-specific data source so it can be used efficiently with Java's parallel Streams API.
Extend Spliterators.AbstractSpliterator (or implement Spliterator directly) and implement tryAdvance() to pull one element and pass it to the given action, and, if the source can be meaningfully divided, override trySplit() to hand back a spliterator over roughly the first half while retaining the rest.
class RangeSpliterator extends Spliterators.AbstractSpliterator<Integer> {
private int current, end;
RangeSpliterator(int start, int end) {
super(end - start, ORDERED | SIZED | SUBSIZED);
this.current = start; this.end = end;
}
public boolean tryAdvance(Consumer<? super Integer> action) {
if (current >= end) return false;
action.accept(current++);
return true;
}
public Spliterator<Integer> trySplit() {
int mid = current + (end - current) / 2;
if (mid <= current) return null;
Spliterator<Integer> prefix = new RangeSpliterator(current, mid);
current = mid;
return prefix;
}
}
38. Given a custom class that already implements Iterable, show how to expose a Stream over it using StreamSupport.stream(), and explain why Iterable does not have a stream() method directly.
StreamSupport.stream() takes a Spliterator and a boolean flag for parallel or sequential execution. Since any Iterable can supply a default Spliterator via spliteratorUnknownSize() wrapping its Iterator, this is the standard bridge. Iterable itself lacks a stream() method by design, since adding one would have forced every existing implementer, going back to Java's earliest collections, to either accept a suboptimal default or be recompiled, so the JDK left it as a call site convenience instead.
class MyCollection<T> implements Iterable<T> {
// ... iterator() implementation ...
Stream<T> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
39. Explain specifically what Spliterator adds beyond Iterator that makes it suitable for parallel decomposition, when a plain Iterator is not.
A plain Iterator only knows how to produce the next single element; it has no concept of dividing its remaining work into independent chunks. Spliterator adds exactly that capability through trySplit(), which returns a new spliterator over some prefix of the remaining elements while the original spliterator retains the rest, letting a fork/join-based framework recursively divide a source across as many worker threads as are useful, guided by estimateSize() to decide when splitting further stops paying off.
40. Contrast the laziness of a Stream pipeline with the eager, step-at-a-time nature of a plain Iterator-driven while loop, using a concrete example involving map() and filter().
A hand-written Iterator-driven loop that calls map then filter as two separate passes fully materializes an intermediate list after the map step before filtering begins. A Stream pipeline instead pulls one element at a time through the entire chain of operations before moving to the next, meaning map and filter are interleaved per-element rather than run as separate full passes, and elements that fail an early filter never even reach a later, more expensive map step.
List<String> result = names.stream()
.filter(n -> n.length() > 3) // runs first, per element
.map(String::toUpperCase) // only runs on survivors
.collect(Collectors.toList()); // nothing executes until this terminal op
41. Explain why a Stream is described as "one-shot": what happens if you attempt to reuse the same Stream instance for a second terminal operation?
Once a terminal operation such as collect() or forEach() consumes a stream, the stream is marked as closed internally, and any further operation on that same stream instance throws IllegalStateException with a message indicating the stream has already been operated upon or closed. This mirrors an exhausted Iterator, which cannot be rewound either, but Streams enforce it explicitly rather than simply having hasNext() return false silently.
Stream<String> stream = names.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // throws IllegalStateException
42. What thread-safety concerns arise when a parallel stream's lambda operations mutate shared external state, and how does this differ from the thread-safety concerns of a plain sequential Iterator?
A sequential Iterator only needs its own internal state protected if multiple threads share the same iterator instance; the client's per-element logic runs on a single thread and is otherwise unaffected. A parallel stream, by contrast, may run the same lambda concurrently across multiple worker threads pulled from the common ForkJoinPool, so any lambda that writes to a shared, non-thread-safe field, such as accumulating into a plain ArrayList via forEach, introduces a genuine data race that a purely sequential mental model would miss.
List<String> unsafe = new ArrayList<>();
names.parallelStream().forEach(unsafe::add); // race: ArrayList is not thread-safe
// correct: let the framework handle the accumulation safely
List<String> safe = names.parallelStream().collect(Collectors.toList());
43. What happens if you call next() on an Iterator that has already been fully exhausted, and can an exhausted iterator ever be "reset" to iterate the same elements again?
Calling next() on an exhausted iterator throws NoSuchElementException per the standard contract, every time, indefinitely. There is no standard mechanism to reset an existing Iterator instance back to the start; the only supported way to iterate the same elements again is to call the aggregate's iterator() method a second time to obtain a fresh instance, assuming the aggregate itself still exists and its iterator() method supports being called more than once (which most collections do, but a genuinely one-shot data source, like a network stream, may not).
44. Compare the legacy Enumeration interface to Iterator: what methods does each expose, and why was Iterator introduced as its replacement in the Collections Framework?
Enumeration exposes hasMoreElements() and nextElement(), predating the Collections Framework and generics. Iterator, introduced with the Collections Framework in Java 1.2, renamed those methods to the now-familiar hasNext()/next() and, critically, added a remove() method, letting an iterator safely mutate its source during traversal, a capability Enumeration never had at all.
interface Enumeration<E> {
boolean hasMoreElements();
E nextElement();
}
interface Iterator<E> {
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException("remove"); }
}
45. A legacy codebase still exposes several methods returning Enumeration, such as Hashtable.elements(). Describe how you would migrate calling code to Iterator without touching the legacy class itself.
Rather than rewriting the legacy class, wrap its Enumeration in a small adapter implementing Iterator, mapping hasMoreElements() to hasNext() and nextElement() to next(), with remove() throwing UnsupportedOperationException since the underlying Enumeration never supported removal in the first place.
class EnumerationIteratorAdapter<T> implements Iterator<T> {
private final Enumeration<T> enumeration;
EnumerationIteratorAdapter(Enumeration<T> enumeration) { this.enumeration = enumeration; }
public boolean hasNext() { return enumeration.hasMoreElements(); }
public T next() { return enumeration.nextElement(); }
}
// new call sites use modern Iterator/Iterable-friendly code
Iterator<String> modern = new EnumerationIteratorAdapter<>(legacyHashtable.elements());
46. Implement an infinite lazy iterator that produces the Fibonacci sequence on demand, one number per call to next(), without ever precomputing or storing the full sequence.
Since the sequence is unbounded, hasNext() simply always returns true, and each call to next() computes the next value from only the two most recently produced values, discarding everything older. This keeps memory usage constant, O(1), regardless of how many terms a caller ultimately consumes.
class FibonacciIterator implements Iterator<Long> {
private long previous = 0, current = 1;
public boolean hasNext() { return true; } // infinite sequence
public Long next() {
long result = previous;
long next = previous + current;
previous = current;
current = next;
return result;
}
}
// usage: take only the first ten values
Iterator<Long> fib = new FibonacciIterator();
for (int i = 0; i < 10; i++) System.out.println(fib.next());
47. Explain the design considerations for an iterator that fetches the next page of a remote API result set on demand, including how to handle a network failure that occurs mid-fetch inside hasNext().
Because hasNext() must fetch to know whether more data exists, it inherits all the failure modes of a network call: timeouts, rate limits, and transient errors. The cleanest design lets the network exception propagate unchecked from hasNext() (typically wrapped as an unchecked exception, since Iterator's methods declare no checked exceptions), and documents clearly that a caller must be prepared to catch it, since a single failed page fetch should not silently look identical to reaching the natural end of the data.
public boolean hasNext() {
if (buffer.hasRemaining()) return true;
if (!hasMorePages) return false;
try {
loadNextPage(); // may throw an unchecked ApiException
} catch (IOException e) {
throw new ApiFetchException("Failed to load next page", e);
}
return buffer.hasRemaining();
}
48. Implement a composite iterator that transparently walks a tree of nested collections, such as a folder structure containing both files and subfolders, presenting a single flat traversal to the client.
This directly connects the Iterator pattern to the Composite pattern: when the current element is itself a composite (a subfolder), the iterator recursively delegates to that composite's own iterator instead of trying to yield the subfolder itself as a leaf value, so the client sees one uniform stream of leaf elements regardless of nesting depth.
interface FileSystemEntry { }
class FileEntry implements FileSystemEntry { String name; }
class FolderEntry implements FileSystemEntry, Iterable<FileEntry> {
private final List<FileSystemEntry> children = new ArrayList<>();
public Iterator<FileEntry> iterator() {
return new Iterator<FileEntry>() {
private final Iterator<FileSystemEntry> childIt = children.iterator();
private Iterator<FileEntry> currentSubIterator = Collections.emptyIterator();
public boolean hasNext() {
while (!currentSubIterator.hasNext() && childIt.hasNext()) {
FileSystemEntry next = childIt.next();
currentSubIterator = (next instanceof FolderEntry folder)
? folder.iterator()
: List.of((FileEntry) next).iterator();
}
return currentSubIterator.hasNext();
}
public FileEntry next() {
if (!hasNext()) throw new NoSuchElementException();
return currentSubIterator.next();
}
};
}
}
49. Design a "zip" iterator that walks two collections in parallel, producing pairs of corresponding elements, and explain how it should behave when the two source collections have different lengths.
A zip iterator wraps two source iterators internally and, on each call to next(), advances both and returns a pair combining their results. Since the two sources may not be the same length, the iterator must decide, and document, a policy: the common choice is to stop as soon as either source is exhausted, meaning hasNext() returns true only while both underlying iterators still have elements.
class ZipIterator<A, B> implements Iterator<Map.Entry<A, B>> {
private final Iterator<A> left; private final Iterator<B> right;
ZipIterator(Iterator<A> left, Iterator<B> right) { this.left = left; this.right = right; }
public boolean hasNext() { return left.hasNext() && right.hasNext(); }
public Map.Entry<A, B> next() {
if (!hasNext()) throw new NoSuchElementException();
return Map.entry(left.next(), right.next());
}
}
50. Discuss whether a custom Iterator's next() method should ever be permitted to return null, and how you would design an iterator over a source that legitimately contains null elements.
Returning null from next() is technically permitted by the interface, since Iterator is not itself generic-bounded against nullability, but it is dangerous because callers cannot then distinguish "the next real element happens to be null" from a bug where the iterator incorrectly returned null instead of throwing NoSuchElementException. If the underlying source legitimately contains nulls, either wrap each element in Optional<T> so absence-of-value and null-value are distinguishable, or document explicitly that null is a valid element and that exhaustion is signaled only by NoSuchElementException, never by a null return.
51. Implement a filtering iterator wrapper that skips elements not matching a given predicate, so a client sees only elements that pass the filter as if the excluded ones did not exist.
The tricky part is that hasNext() cannot just check the wrapped iterator's hasNext() directly, since the very next element in the source might fail the predicate; it must actively search forward and buffer the first matching element it finds, exactly like the read-ahead pattern used for unknown-size sources.
class FilteringIterator<T> implements Iterator<T> {
private final Iterator<T> source; private final Predicate<T> predicate;
private T nextMatch; private boolean hasBuffered = false;
FilteringIterator(Iterator<T> source, Predicate<T> predicate) {
this.source = source; this.predicate = predicate;
}
public boolean hasNext() {
if (hasBuffered) return true;
while (source.hasNext()) {
T candidate = source.next();
if (predicate.test(candidate)) { nextMatch = candidate; hasBuffered = true; return true; }
}
return false;
}
public T next() {
if (!hasNext()) throw new NoSuchElementException();
hasBuffered = false;
return nextMatch;
}
}
52. Implement a "peekable" iterator that lets a client look at the next element without consuming it, and explain what internal state this requires beyond a plain Iterator.
A peekable iterator needs one extra field: a buffered "peeked" value plus a flag indicating whether that buffer is currently populated. peek() fills the buffer if empty and returns its contents without advancing; next() returns the buffered value if present (clearing the buffer) or otherwise delegates straight to the source.
class PeekingIterator<T> implements Iterator<T> {
private final Iterator<T> source;
private T peeked; private boolean hasPeeked = false;
PeekingIterator(Iterator<T> source) { this.source = source; }
public T peek() {
if (!hasPeeked) { peeked = source.next(); hasPeeked = true; }
return peeked;
}
public boolean hasNext() { return hasPeeked || source.hasNext(); }
public T next() {
if (hasPeeked) { hasPeeked = false; return peeked; }
return source.next();
}
}
53. Discuss whether it makes sense to add a reset() or rewind() method to a custom Iterator implementation, and what trade-offs this introduces against the standard one-pass contract.
The standard Iterator contract is deliberately one-pass and forward-only; adding reset() is possible for sources that can cheaply restore their starting position, such as an in-memory array-backed iterator, but breaks the implicit expectation most calling code has that once exhausted, an iterator stays exhausted. It also complicates the fail-fast contract, since "resetting" is arguably itself a kind of state change that other in-flight consumers of the same iterator instance would not expect.
In practice, it is usually cleaner to expose a resettable capability by simply calling the aggregate's iterator() method again to obtain a brand-new iterator, rather than mutating an existing iterator back to its starting state, keeping the standard contract intact for callers who only expect a plain forward-only Iterator.
54. Design an iterator that supports checkpointing and resuming a long-running traversal, for example over a multi-gigabyte dataset, so processing can restart from the last known position after a crash.
The key design decision is making the cursor state, whatever uniquely identifies "where the traversal currently is," externally serializable and independent of any in-memory object references. For a paginated remote source this is naturally the last-seen page token or offset; for an in-memory structure it might be a numeric index. The iterator exposes a method to obtain this checkpoint, and a corresponding factory method (rather than a public constructor with a raw index) to resume an iterator from a previously saved checkpoint.
class ResumableIterator implements Iterator<Record> {
private long offset;
ResumableIterator(long startOffset) { this.offset = startOffset; }
static ResumableIterator resumeFrom(long savedOffset) { return new ResumableIterator(savedOffset); }
long checkpoint() { return offset; } // persist this externally after each batch
public boolean hasNext() { return offset < totalRecordCount(); }
public Record next() { return fetchRecordAt(offset++); }
}
55. Implement hasPrevious() and previous() for a custom ListIterator-style bidirectional iterator over an array-backed structure, and explain the relationship between the forward and backward cursor positions.
A single cursor index conceptually sits "between" two elements: next() returns the element to the cursor's right and advances the cursor rightward, while previous() returns the element to the cursor's left and moves the cursor leftward. This means calling next() immediately followed by previous() returns the same element twice, which is the documented, expected behavior for ListIterator, not a bug.
public boolean hasPrevious() { return cursor > 0; }
public T previous() {
if (!hasPrevious()) throw new NoSuchElementException();
return data[--cursor];
}
public boolean hasNext() { return cursor < size; }
public T next() {
if (!hasNext()) throw new NoSuchElementException();
return data[cursor++];
}
56. Implement an iterator over a graph structure that may contain cycles, ensuring the traversal terminates correctly and never revisits the same node twice.
Unlike a tree, a graph can have cycles, so a naive recursive or stack-based walk that does not track history can loop forever. The iterator must maintain a visited-node set (typically an identity-based Set, or one keyed by a unique node id) and, before enqueuing or yielding any neighbor, check whether it has already been visited or is already queued for visiting.
class GraphBfsIterator<T> implements Iterator<T> {
private final Deque<GraphNode<T>> queue = new ArrayDeque<>();
private final Set<GraphNode<T>> visited = Collections.newSetFromMap(new IdentityHashMap<>());
GraphBfsIterator(GraphNode<T> start) { queue.add(start); visited.add(start); }
public boolean hasNext() { return !queue.isEmpty(); }
public T next() {
if (!hasNext()) throw new NoSuchElementException();
GraphNode<T> node = queue.poll();
for (GraphNode<T> neighbor : node.neighbors()) {
if (visited.add(neighbor)) queue.add(neighbor); // add() returns false if already present
}
return node.value();
}
}
57. Implement an iterator that concatenates several existing iterators into one continuous logical sequence, moving to the next source iterator only once the current one is exhausted.
Hold a queue or list of the source iterators, and whenever the current one reports hasNext() == false, advance to the next non-empty one in the sequence before reporting exhaustion. The check must be applied repeatedly, since several consecutive source iterators could all be empty.
class ConcatIterator<T> implements Iterator<T> {
private final Deque<Iterator<T>> remaining;
ConcatIterator(List<Iterator<T>> sources) { remaining = new ArrayDeque<>(sources); }
public boolean hasNext() {
while (!remaining.isEmpty() && !remaining.peek().hasNext()) remaining.poll();
return !remaining.isEmpty();
}
public T next() {
if (!hasNext()) throw new NoSuchElementException();
return remaining.peek().next();
}
}
58. Design an iterator that internally reads data in fixed-size batches from a backing store, such as a database cursor, but yields elements to the client one at a time.
This decouples the backing store's efficient access pattern (bulk fetches, amortizing per-round-trip overhead) from the client's simple one-at-a-time consumption model. Internally the iterator keeps a small in-memory buffer of the current batch and an index into it, refilling the buffer with the next batch only once the current one is exhausted, mirroring the read-ahead pattern used for the paginated remote result set.
class BatchedDbIterator implements Iterator<Row> {
private final int batchSize; private List<Row> buffer = List.of();
private int bufferIndex = 0; private long nextOffset = 0; private boolean exhausted = false;
public boolean hasNext() {
if (bufferIndex < buffer.size()) return true;
if (exhausted) return false;
buffer = fetchBatch(nextOffset, batchSize);
bufferIndex = 0; nextOffset += buffer.size();
if (buffer.size() < batchSize) exhausted = true;
return !buffer.isEmpty();
}
public Row next() {
if (!hasNext()) throw new NoSuchElementException();
return buffer.get(bufferIndex++);
}
}
59. Explain how bounded wildcards, such as Iterator<? extends Number>, affect the design of a generic method that consumes an arbitrary iterator, and why this matters for API flexibility.
A method parameter typed as Iterator<Number> only accepts an iterator whose exact declared element type is Number, rejecting a perfectly usable Iterator<Integer> even though every Integer is a Number. Declaring the parameter as Iterator<? extends Number> follows the standard "producer extends" guideline: since the method only reads (produces) values from the iterator via next() and never needs to insert a value of a specific type into it, the wildcard lets callers pass an iterator over any subtype of Number.
double sumAll(Iterator<? extends Number> values) {
double total = 0;
while (values.hasNext()) total += values.next().doubleValue();
return total;
}
sumAll(integerList.iterator()); // works thanks to the wildcard
sumAll(doubleList.iterator()); // also works
60. Is it safe to iterate a plain, non-thread-safe HashMap on one thread while another thread concurrently adds entries to it, and what would you recommend instead?
It is not safe: HashMap gives no thread-safety guarantees at all, and concurrent structural modification during iteration can, in the worst case, corrupt the map's internal bucket/bin structure (historically even capable of causing an infinite loop during table resizing under concurrent writes), not merely throw a clean ConcurrentModificationException. The fail-fast check is a best-effort debugging aid, not a safety mechanism, as covered earlier, and it offers no protection against genuinely concurrent access.
The correct fix is to use a collection designed for concurrent access, such as ConcurrentHashMap, whose weakly consistent iterators are safe under concurrent modification by design, rather than attempting to add ad hoc synchronization around a plain HashMap's iteration, which is easy to get subtly wrong.
61. Explain the difference between the Iterator pattern and the Visitor pattern, since both traverse a structure, and clarify exactly what each pattern externalizes to the client.
Both patterns pull traversal-related logic out of the aggregate, but they externalize different things. Iterator externalizes the traversal control itself: the client decides when to advance and what to do with each element, while the aggregate's internal structure stays hidden. Visitor externalizes the operation performed at each element: the traversal walk usually still happens inside the structure (via an accept() method on each node), but what happens to each element, the actual behavior, is supplied externally as a Visitor object, often to support different operations (rendering, exporting, validating) over the same fixed structure without modifying its classes.
// Iterator: client controls traversal, decides what to do per element
Iterator<Node> it = tree.iterator();
while (it.hasNext()) { render(it.next()); }
// Visitor: structure controls traversal, client supplies the operation
tree.accept(new RenderingVisitor());
tree.accept(new ExportVisitor()); // same traversal, different behavior
62. Is the Java Streams API essentially "the modern Iterator pattern"? Give a nuanced answer that credits the similarities without overstating them.
There is real overlap: both abstract "walk these elements without exposing internal representation," and Streams are literally built on top of Spliterator, a direct descendant of Iterator. But calling Streams simply "the modern Iterator" overstates the similarity, since Streams add capabilities the original pattern never addressed: declarative operation composition (map/filter/reduce), automatic laziness and operation fusion, and built-in parallel decomposition.
A more precise framing: Streams are a higher-level abstraction built on the same underlying idea as Iterator, internal rather than external iteration, purpose-built for composing declarative data-processing pipelines, whereas classic Iterator remains the right tool when a client genuinely needs fine-grained external control over a traversal, such as pausing, checkpointing, or interleaving it with unrelated logic.
63. Summarize, side by side, the differences between Iterator, the legacy Enumeration, Spliterator, and Streams as the four main traversal mechanisms available in Java.
Each represents a different point in the evolution of traversal in the JDK: Enumeration was the original, minimal, read-only mechanism; Iterator replaced it with modern naming plus safe removal support; Spliterator generalized Iterator to support splitting for parallelism; and Streams layered a declarative, lazily-evaluated pipeline API on top of Spliterator.
| Mechanism | Introduced | Key capability |
|---|---|---|
| Enumeration | Java 1.0 | Read-only sequential access; no remove() |
| Iterator / Iterable | Java 1.2 (Collections Framework) | Safe removal via remove(); external, client-driven control |
| Spliterator | Java 8 | Splittable traversal for parallel decomposition |
| Streams | Java 8 | Lazy, composable, potentially parallel pipelines built on Spliterator |
64. When would a plain index-based for loop over a List be preferable to using its Iterator, and when does that same index-based approach become a performance trap?
For an array-backed, random-access list such as ArrayList, an index-based loop performs essentially identically to an Iterator-based loop, and is arguably simpler when you also need the index value itself for logging or array alignment. It becomes a serious performance trap for a LinkedList, where get(index) must walk from the head (or tail, whichever is closer) every single call, turning what looks like an O(n) loop into an O(n²) traversal overall, whereas the Iterator maintains a direct node reference and walks in true O(n).
List<String> linked = new LinkedList<>(hugeDataset);
for (int i = 0; i < linked.size(); i++) {
process(linked.get(i)); // O(n) per call -> O(n^2) overall, very slow
}
// correct for LinkedList: use its Iterator, which walks node-to-node in O(n) total
for (String item : linked) { process(item); }
65. Compare the Iterator pattern to a database cursor, since both provide sequential, stateful access to a data source, and note the key structural differences.
Conceptually they are close relatives: both hold a position within a larger dataset and advance one record or element at a time without requiring the whole dataset in memory. The key differences are that a database cursor is typically tied to an open server-side or driver-side resource (a transaction, a network connection, or server memory) that must be explicitly closed to release those resources, while a plain in-memory Iterator generally has no external resource to release and can simply be garbage collected once dropped.
A JDBC ResultSet is essentially a cursor exposed with an Iterator-like API (next() advances and returns a boolean), and understanding this parallel is useful for recognizing when a custom iterator wrapping a similarly resource-backed source needs the same explicit-close discipline.
66. Compare using Iterable.forEach() versus an explicit while (iterator.hasNext()) loop for the same traversal, including any subtle behavioral difference between them.
Both ultimately walk the same elements, but forEach() is internal iteration: the collection drives the callback and the client cannot break out of it early with a plain break statement, only by throwing an exception from within the lambda. An explicit hasNext()/next() loop is external iteration and supports normal control flow, including break, continue, and calling remove() on the iterator mid-loop, none of which forEach() supports directly.
// forEach: cannot break early without throwing
list.forEach(item -> { if (item.isBad()) throw new StopIterationException(); process(item); });
// explicit iterator: natural early exit and mutation support
Iterator<Item> it = list.iterator();
while (it.hasNext()) {
Item item = it.next();
if (item.isBad()) break; // trivial
process(item);
}
67. Address a common interview misconception: is the Iterator pattern related to the Observer pattern, since both involve a sequence of things happening over time?
They are unrelated despite the surface-level similarity of "a sequence of things happening." Iterator is about pulling elements from an already-existing, finite (or possibly infinite but enumerable) aggregate at the client's own pace, one at a time, on demand. Observer is about pushing notifications from a subject to registered listeners whenever an event occurs, with no client-driven pull mechanism and no concept of a fixed underlying collection being walked.
A useful way to distinguish them in an interview: Iterator answers "give me the next element of this collection," while Observer answers "tell me whenever something happens," and conflating the two usually signals a candidate pattern-matching by surface similarity rather than by underlying intent.
68. Address another common misconception: how does the Iterator pattern differ from the Command pattern, given that both can be said to "encapsulate an action"?
Command encapsulates a single request or action, its receiver, and its parameters, as an object that can be queued, logged, undone, or executed later; it has nothing inherently to do with walking a sequence of elements. Iterator encapsulates the state and logic needed to traverse a collection's elements one at a time; it is not "an action to be executed" in the Command sense, it is a traversal cursor.
They can appear together, for example iterating over a list of Command objects to execute each one in order for an undo/redo history, but that is simply Iterator traversing a collection whose elements happen to be Commands, not the two patterns being conceptually related.
69. Describe a scenario where introducing a custom Iterator implementation would be unnecessary over-engineering, and a plain array or index-based loop would be the better choice.
If the data is a small, fixed-size, in-memory array or list that will only ever be walked forward, once, with no need for lazy generation, no need to hide the storage mechanism from any external caller, and no plans to ever change the underlying representation, writing a custom Iterator class adds an extra layer of indirection and boilerplate that buys nothing. A plain index-based loop or the built-in Iterable support already provided by List or arrays is simpler, easier to read, and just as efficient.
70. When is it sufficient to implement only Iterable on a domain class (returning a custom Iterator), versus implementing the full java.util.Collection interface?
Implementing only Iterable is sufficient, and preferable, when the domain class's primary purpose is something other than being a general-purpose collection, and you only need to support read-only, for-each-style traversal, without needing size(), contains(), bulk operations, or interoperability with every method that accepts a Collection. Implementing the full Collection interface is the right call when client code genuinely needs to pass your type anywhere a Collection is expected, such as into Collections.sort() or a constructor that accepts Collection<? extends T>.
A common middle ground is to implement Iterable directly on a domain object (such as an Order that is iterable over its LineItems) while exposing an actual List or Collection for the cases where full collection semantics are genuinely needed, rather than forcing the domain object itself to satisfy the entire Collection contract.
71. How does the concept of lazy sequences in functional languages relate to Java's Iterator, and is this comparison useful to raise in an interview?
Functional languages' lazy sequences (such as Haskell's lazy lists or Clojure's lazy-seqs) and Java's Iterator share the same core idea: elements are computed on demand rather than all at once, enabling infinite or expensive sequences to be represented without materializing them upfront, exactly as demonstrated by the Fibonacci iterator example. The comparison is useful in an interview because it shows you understand that "lazy, on-demand traversal" is a general concept, not something Java's Iterator invented, and that Java's own Streams API is the language's move toward that same functional style of composable, lazy pipelines.
The main difference is persistence and purity: functional lazy sequences are typically immutable and safely shareable, since evaluating them has no side effects, while a Java Iterator is inherently stateful and mutates its own cursor on every call, so two references to the same Java iterator are not safely shareable the way a lazy sequence often is.
72. Explain what Guava's PeekingIterator adds over java.util.Iterator, and how it compares to the custom peekable iterator shown earlier.
Guava's PeekingIterator<E> extends Iterator<E> with exactly one additional method, peek(), that returns the next element without consuming it, implemented with the same lookahead-buffering strategy discussed earlier for a hand-rolled peekable iterator. Guava provides it as a well-tested, reusable utility via Iterators.peekingIterator(iterator), so teams that need this capability across a codebase do not need to hand-roll and separately test the buffering logic themselves.
PeekingIterator<String> it = Iterators.peekingIterator(list.iterator());
if (it.hasNext() && it.peek().equals("skip-me")) {
it.next(); // consume and discard the peeked value
}
73. What is the purpose of the RandomAccess marker interface, and how should an algorithm operating on a List decide between index-based access and Iterator-based access based on it?
RandomAccess is an empty marker interface, implemented by ArrayList but not by LinkedList, that signals a list supports fast, constant-time random access via get(index). Generic algorithms that must work efficiently across any List implementation, such as those inside Collections, check instanceof RandomAccess at runtime and choose an index-based loop when true, or an Iterator-based loop when false, avoiding the O(n²) trap described earlier for index-based access on a LinkedList.
if (list instanceof RandomAccess) {
for (int i = 0; i < list.size(); i++) process(list.get(i));
} else {
for (Object item : list) process(item); // Iterator-based, safe for LinkedList
}
74. Identify a scenario where the enhanced for-each loop is actively the wrong tool, forcing you back to an explicit Iterator or index-based loop instead.
The for-each loop is the wrong choice whenever the loop body needs to remove elements from the collection being traversed (it has no access to the underlying Iterator's remove()), whenever it needs the current numeric index for something other than the element itself, such as reporting "processing item 3 of 10," or whenever it needs to walk two related collections in lockstep by shared index. In every one of these cases, an explicit Iterator (or ListIterator, or an indexed loop) is required instead.
75. Explain the role Iterator plays specifically within the Composite pattern's tree structures, and why a Composite implementation typically needs a custom iterator rather than relying on a built-in collection's default one.
A Composite node's "elements" are really a mix of leaves and other composites nested arbitrarily deep, so a default, single-level Iterator over the node's direct children only ever sees one level of the tree, not the full flattened set of leaves the client usually wants. As shown earlier for the folder/file example, the Composite's own iterator() must recursively delegate into child composites' iterators, presenting one flattened, uniform stream of leaf elements to the client regardless of how the tree is actually shaped underneath.
76. How would you write a unit test that verifies an Iterator implementation visits every element exactly once, in the correct order, including edge cases like an empty collection?
The most reliable technique is to drain the iterator into a plain List by looping while (it.hasNext()) and calling add(it.next()) on an accumulator, then assert that accumulator equals the expected ordered list with assertEquals. This single assertion covers both "every element visited" and "in the correct order" at once, since list equality is order-sensitive. A second assertion should confirm that after the loop ends, hasNext() keeps returning false on repeated calls rather than flipping back to true, and a dedicated empty-collection test should assert hasNext() returns false immediately with no prior next() call at all.
@Test
void iteratesAllElementsInOrder() {
MyCollection<String> coll = MyCollection.of("a", "b", "c");
List<String> seen = new ArrayList<>();
Iterator<String> it = coll.iterator();
while (it.hasNext()) {
seen.add(it.next());
}
assertEquals(List.of("a", "b", "c"), seen);
assertFalse(it.hasNext()); // stays exhausted
}
@Test
void emptyCollectionHasNoElements() {
assertFalse(MyCollection.of().iterator().hasNext());
}
77. Write a test that verifies calling next() on an exhausted Iterator throws NoSuchElementException rather than returning null or looping back to the start.
This is a contract test every custom iterator implementation should carry, since the Iterator interface's Javadoc explicitly requires next() to throw NoSuchElementException once the iteration has no more elements, and silently returning null or wrapping around is a common, subtle bug that only surfaces when a caller iterates one element too far. assertThrows (JUnit 5) makes the expectation explicit and self-documenting rather than relying on a manually caught exception.
@Test
void nextThrowsAfterExhaustion() {
Iterator<String> it = MyCollection.of("only").iterator();
it.next(); // consumes the one element
assertThrows(NoSuchElementException.class, it::next);
}
78. Diagnose the bug in code that calls iterator.next() in a loop without first checking hasNext(), and explain how to fix it.
Calling next() unconditionally, for example inside a fixed-count for loop or a while (true) loop that relies on catching an exception to stop, assumes the caller already knows exactly how many elements exist. As soon as that assumption is wrong, whether because the collection is smaller than expected, was concurrently drained, or the count was miscalculated, the next call past the last element throws NoSuchElementException and crashes the loop instead of terminating gracefully.
// Buggy: assumes exactly `count` elements exist
for (int i = 0; i < count; i++) {
process(it.next()); // throws if collection has fewer than `count` items
}
// Fixed: let hasNext() be the sole authority on when to stop
while (it.hasNext()) {
process(it.next());
}
next() call behind hasNext().79. Diagnose a bug where calling iterator.remove() throws IllegalStateException, and explain the exact rule that causes it.
The Iterator.remove() contract requires that next() has been called at least once since the iterator was created, and that remove() has not already been called since that next(). Violating either half of that rule, calling remove() before ever calling next(), or calling remove() twice in a row without an intervening next(), throws IllegalStateException. The underlying reason is that remove() deletes "the element most recently returned by next()", so there must be exactly one such element outstanding at the time it is called.
Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException: next() never called
it.next();
it.remove();
it.remove(); // IllegalStateException: already removed this element
80. Explain how holding a reference to an Iterator (or an unclosed custom iterator) after use can cause a memory leak, and how to avoid it.
A plain Iterator over an in-memory collection generally holds a reference back to that collection (an ArrayList$Itr keeps its outer ArrayList reference, for instance). If code stashes a finished iterator in a long-lived field, a static cache, or a closure that outlives the loop, it inadvertently keeps the entire backing collection reachable from garbage collection roots long after the loop is done, even if every other reference to that collection has gone out of scope. This is the same class of leak as an unclosed resource, just triggered by an object reference rather than a file handle.
// Leak: iterator escapes into a field that outlives the loop
this.lastIterator = someHugeList.iterator(); // keeps someHugeList alive indefinitely
// Fix: scope the iterator to the loop only, never store it
for (String s : someHugeList) { process(s); } // iterator is local and discarded
81. Why is it considered bad practice for an Iterator's next() method to have external side effects (e.g., mutating the underlying collection or logging with I/O), and what bug pattern does this create?
Callers, and JDK internals such as forEach() or Collectors, generally assume that iterating a source is observationally passive: reading it once should behave predictably regardless of how many times library code happens to invoke hasNext() internally, or whether an iteration is retried after a partial failure. A next() that writes to a database, increments a shared counter, or has any other externally-visible effect breaks that assumption; retrying a partially-consumed stream (a common pattern after a transient exception) then silently double-applies those side effects, since the iterator has no way to "undo" already-emitted side effects for elements a caller re-reads.
// Bug: next() increments a shared audit counter as a side effect
public String next() {
auditCounter.incrementAndGet(); // fires again on any retry of a partial read
return backing.get(cursor++);
}
82. Diagnose a bug in a custom Iterator where calling hasNext() multiple times in a row before calling next() produces wrong results, and explain why hasNext() must be idempotent.
A correct hasNext() must be a pure, side-effect-free query: it may only peek at whether more elements remain, never advance the cursor or consume an element itself. A buggy implementation that conflates "check" with "advance", for example one written for a data source without random lookahead that mistakenly pulls the next element inside hasNext() and discards it if called again, silently skips elements whenever calling code (or JDK internals, which frequently call hasNext() speculatively before calling next()) invokes hasNext() more than once between next() calls.
// Buggy: hasNext() consumes from the source every call
public boolean hasNext() {
current = source.readNext(); // BUG: advances source, not idempotent
return current != null;
}
// Fixed: cache the peeked value so repeated hasNext() calls are safe
private T buffered;
private boolean hasBuffered;
public boolean hasNext() {
if (!hasBuffered) {
buffered = source.readNext();
hasBuffered = (buffered != null);
}
return hasBuffered;
}
public T next() {
if (!hasNext()) throw new NoSuchElementException();
hasBuffered = false;
return buffered;
}
83. Walk through how you would diagnose a ConcurrentModificationException that only appears intermittently in production but never in local testing.
Intermittent CMEs are almost always a symptom of genuine concurrent access that only manifests under production-level load or timing, since local tests rarely exercise the exact interleaving needed to trigger it. The diagnostic sequence: first capture the full stack trace from the production incident, which pinpoints the exact iteration site that hit the comodification check; second, search that site's surrounding code and any classes it calls into for anything that could structurally modify the same collection concurrently, common culprits are a background scheduled task (cache eviction, a metrics flush), an event listener callback invoked synchronously from within the loop body that mutates the same list, or a second request thread sharing a non-thread-safe field.
Once a suspect is found, reproduce it deliberately with a small concurrent stress test that hammers both the read and the suspected mutation path in tight loops, which usually reproduces the CME reliably in seconds even though it took hours to surface in production. The fix is then either genuine synchronization, switching to a concurrent collection appropriate to the access pattern (CopyOnWriteArrayList for read-heavy/write-rare, ConcurrentHashMap for maps), or eliminating the concurrent mutation path entirely.
84. Explain how a naive Iterator implementation that eagerly loads or retains an entire large dataset can cause the same memory pressure the pattern is normally used to avoid, and how to fix it.
Iterator's whole value proposition for large or unbounded sources is lazy, on-demand production of one element at a time without ever materializing the full dataset in memory. A naive implementation defeats this entirely if its constructor (or its iterator() factory method) eagerly calls something like .toList() on the whole source upfront "to make hasNext()/next() simpler to write", or if a lookahead-buffering iterator mistakenly buffers the entire remaining dataset instead of just the single next element. Either mistake reintroduces the exact "load everything into memory at once" problem the pattern exists to solve, just hidden behind an Iterator-shaped API.
// Defeats the purpose: loads the whole multi-GB file before iterating a byte
public FileLineIterator(Path file) throws IOException {
this.allLines = Files.readAllLines(file); // BUG: entire file in memory
this.cursor = 0;
}
// Correct: hold only an open reader and the one next line
public FileLineIterator(Path file) throws IOException {
this.reader = Files.newBufferedReader(file);
this.nextLine = reader.readLine(); // buffers exactly one line ahead
}
85. Design an Iterator that wraps a resource requiring cleanup (such as an open file or network stream) after traversal completes or is abandoned early, and explain the tricky part of guaranteeing cleanup.
The straightforward part is closing the underlying resource automatically once hasNext() detects natural end-of-stream, since that is a moment the iterator itself controls. The genuinely tricky part is the abandoned-early case: if a caller stops calling next()/hasNext() partway through (a break in a for-each loop, an early return, an exception thrown mid-loop), plain Iterator has no lifecycle hook that fires on abandonment, so end-of-stream auto-close never runs and the resource leaks. The only robust fix is to make the iterator itself, or the Iterable that produced it, also implement AutoCloseable and require callers to use it inside a try-with-resources block rather than relying on natural exhaustion.
86. Java's JDBC ResultSet has next() and getXxx() methods that look Iterator-like, yet ResultSet does not implement java.util.Iterator. Explain why.
Several mismatches with the Iterator<T> contract rule it out. First, ResultSet.next() both advances the cursor and reports whether a row is available in one call (returning boolean), collapsing what Iterator splits across two separate methods, hasNext() to peek and next() to advance and retrieve. Second, ResultSet.next() declares a checked SQLException, which Iterator.next()'s signature cannot express. Third, and most fundamentally, a row from a ResultSet is not one value of a single type T, it is a multi-column record accessed through per-column, per-type getters like getString(int) and getInt(int), which does not fit the "produce one T per call" shape Iterator<T> assumes.
while (rs.next()) { // advance + hasNext in one boolean call
String name = rs.getString("name"); // typed, per-column access, not a single T
int age = rs.getInt("age");
}
87. Implement a custom iterator type that combines Iterator<T> and AutoCloseable so a resource-backed sequence can be used safely in a try-with-resources block.
Declaring a small combined interface lets any resource-backed sequence, a file, a socket, a paginated remote cursor, be consumed with the same guarantee a try-with-resources block already gives ordinary Closeable resources, without callers needing to remember any special close-on-exhaustion behavior.
public interface CloseableIterator<T> extends Iterator<T>, AutoCloseable {
@Override void close(); // narrow the throws clause to unchecked, for easy use
}
public class FileLineIterator implements CloseableIterator<String> {
private final BufferedReader reader;
private String nextLine;
public FileLineIterator(Path file) throws IOException {
this.reader = Files.newBufferedReader(file);
this.nextLine = reader.readLine();
}
@Override public boolean hasNext() { return nextLine != null; }
@Override public String next() {
if (!hasNext()) throw new NoSuchElementException();
String line = nextLine;
try { nextLine = reader.readLine(); } catch (IOException e) { throw new UncheckedIOException(e); }
return line;
}
@Override public void close() {
try { reader.close(); } catch (IOException e) { throw new UncheckedIOException(e); }
}
}
// Usage
try (FileLineIterator it = new FileLineIterator(path)) {
while (it.hasNext()) process(it.next());
} // reader.close() guaranteed even on early break or exception
88. Write a test that demonstrates a fail-safe iterator (such as CopyOnWriteArrayList's) does NOT throw ConcurrentModificationException when the underlying list is modified during iteration, and explain what it does show instead.
The test should mutate the backing list from inside the loop body and assert both that no exception is thrown and that the iteration finishes using the elements present at the moment the iterator was created, since CopyOnWriteArrayList's iterator walks a private snapshot array captured at iterator() time and is entirely unaffected by subsequent structural changes to the live list.
@Test
void failSafeIteratorIgnoresConcurrentMutation() {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(List.of("a", "b"));
List<String> seen = new ArrayList<>();
for (String s : list) {
seen.add(s);
list.add("c"); // structural modification mid-iteration: no CME
}
assertEquals(List.of("a", "b"), seen); // snapshot never saw "c"
assertEquals(List.of("a", "b", "c", "c"), list); // live list did grow
}
89. Compare implementing an in-memory Iterator over a large dataset to implementing pagination (keyset/cursor-based) across a REST API or database query, since both provide iteration over data too large to load at once.
Both solve "walk more data than fits in memory or in one response, one chunk at a time," but the state-holding location differs fundamentally. An in-process Iterator keeps its cursor as live object state inside a single JVM for the duration of one method call; a REST or database pagination scheme has no such luxury, since each HTTP request is independent and stateless and the server generally cannot keep a live cursor open across requests, so the "cursor" must instead be serialized into an opaque page token or a last-seen key that the client resends on the next request.
This has a real correctness consequence: naive offset-based pagination (OFFSET 100 LIMIT 20) can skip or duplicate rows if rows are inserted or deleted between page requests, because the offset is just a row count, not a stable position. Keyset (cursor) pagination, resuming from "the last row's unique sort key" rather than a raw offset, avoids that drift the same way a well-behaved in-memory iterator's cursor never loses its place, making it the closer analog to Iterator's semantics across a stateless, multi-request boundary.
| In-memory Iterator | API/DB pagination | |
|---|---|---|
| Cursor state lives in | Object field, one process | Opaque token, resent by client |
| Risk under concurrent writes | None (single-threaded use) or CME | Skips/duplicates unless keyset-based |
90. Implement a generic peekable Iterator wrapper from scratch (without a third-party library) using the lookahead buffer technique.
The lookahead buffer technique wraps a delegate Iterator<T> and eagerly pulls exactly one element ahead into a buffer field the first time it is needed, so peek() can return it repeatedly without consuming it, and next() simply hands back the buffered value and clears the buffer so the next call re-fills it from the delegate.
public class PeekableIterator<T> implements Iterator<T> {
private final Iterator<T> delegate;
private T buffered;
private boolean hasBuffered;
public PeekableIterator(Iterator<T> delegate) { this.delegate = delegate; }
public T peek() {
fillBufferIfNeeded();
if (!hasBuffered) throw new NoSuchElementException();
return buffered;
}
@Override public boolean hasNext() {
fillBufferIfNeeded();
return hasBuffered;
}
@Override public T next() {
if (!hasNext()) throw new NoSuchElementException();
T result = buffered;
hasBuffered = false;
buffered = null;
return result;
}
private void fillBufferIfNeeded() {
if (!hasBuffered && delegate.hasNext()) {
buffered = delegate.next();
hasBuffered = true;
}
}
}
91. What is the correct way to document and defend against callers of a custom Iterable/Iterator misusing remove() when your implementation does not support element removal?
Since Java 8, Iterator.remove() is a default method on the interface itself that already throws UnsupportedOperationException unless a concrete implementation overrides it, so the correct approach for a read-only custom iterator is simply to not override remove() at all, letting the interface's own default do the correct thing, rather than writing a pass-through override that throws the same exception manually (which adds code that can drift from the default's exact message and adds nothing). The class-level or method-level Javadoc should still explicitly state that removal is unsupported, so IDE-level documentation and generated API docs make the limitation discoverable without requiring a caller to hit the exception first.
/**
* Iterates elements in insertion order. This iterator does not support
* {@link #remove()}; calling it always throws UnsupportedOperationException.
*/
public class ReadOnlyIterator<T> implements Iterator<T> {
// no remove() override needed -- Iterator's default already throws correctly
...
}
92. Identify the anti-pattern in an Iterable implementation that returns its internal mutable List's iterator directly (list.iterator()) rather than a purpose-built read-only Iterator, and explain the risk.
Returning this.internalList.iterator() directly from a domain object's iterator() method hands the caller the exact same removal capability the backing ArrayList's iterator has, letting external code call remove() and silently mutate private internal state with no validation, no invariant checks, and no way for the domain object to even know it happened. It also quietly leaks the fact that the internal representation is a mutable List at all, coupling external code to an implementation detail that should be free to change later.
// Anti-pattern: caller can now mutate Order's internals via remove()
public Iterator<LineItem> iterator() {
return this.lineItems.iterator(); // exposes remove() on internal state
}
// Fix: wrap in an unmodifiable view, or a bespoke read-only iterator
public Iterator<LineItem> iterator() {
return Collections.unmodifiableList(this.lineItems).iterator();
}
93. Design a generic, reusable Iterable<T> wrapper class (e.g., a FilteringIterable<T>) that lazily filters another Iterable<T> by a Predicate<T>, without materializing a new collection.
A reusable filtering wrapper needs generics on both the element type and the wrapped source, and its returned Iterator must use the same lookahead-buffering technique as a peekable iterator, advancing the underlying delegate silently until an element matching the predicate is found (or the delegate is exhausted), so that hasNext() never reports true for an element that would actually be rejected.
public class FilteringIterable<T> implements Iterable<T> {
private final Iterable<T> source;
private final Predicate<? super T> predicate;
public FilteringIterable(Iterable<T> source, Predicate<? super T> predicate) {
this.source = source;
this.predicate = predicate;
}
@Override public Iterator<T> iterator() {
return new Iterator<T>() {
private final Iterator<T> delegate = source.iterator();
private T next;
private boolean hasNext;
{ advance(); }
private void advance() {
while (delegate.hasNext()) {
T candidate = delegate.next();
if (predicate.test(candidate)) { next = candidate; hasNext = true; return; }
}
hasNext = false;
}
@Override public boolean hasNext() { return hasNext; }
@Override public T next() {
if (!hasNext) throw new NoSuchElementException();
T result = next;
advance();
return result;
}
};
}
}
94. Discuss how a custom Iterator should handle a collection that legitimately contains null elements, and the specific bug this can cause when combined with common "sentinel" patterns.
A collection whose elements can legitimately be null (a sparse array, a list allowing empty slots) must still rely purely on hasNext() as the sole "is there more" signal; the moment any code path treats "next() returned null" as an informal end-of-data sentinel, a genuine null element becomes indistinguishable from true exhaustion, silently truncating the iteration before the real end. This exact conflation is a classic source of subtle production bugs, since it works perfectly in every test that happens not to include a null element.
// Buggy: treats a returned null as "no more elements"
Object val;
while ((val = it.next()) != null) { process(val); } // stops early on a real null, and
// also breaks past the real end (NoSuchElementException)
// Correct: hasNext() alone decides when to stop, null is a valid payload
while (it.hasNext()) {
Object val = it.next(); // may legitimately be null; still processed
process(val);
}
95. Provide a complete Big-O comparison of ArrayList and LinkedList across get(index), add/remove at the end, add/remove at the beginning, add/remove via a ListIterator mid-traversal, and iteration itself.
The two implementations trade off in almost exactly opposite ways, which is why choosing between them should be driven by the dominant access pattern rather than habit: ArrayList favors random access and amortized append at the cost of expensive insertion/removal away from the end; LinkedList favors cheap insertion/removal anywhere once positioned there (via its ListIterator) at the cost of expensive random access by index.
| Operation | ArrayList | LinkedList |
|---|---|---|
| get(index) | O(1) | O(n) |
| add/remove at end | O(1) amortized | O(1) |
| add/remove at beginning | O(n) (shifts all elements) | O(1) |
| add/remove via positioned ListIterator | O(n) (still shifts elements) | O(1) |
| full sequential iteration | O(n), best cache locality | O(n), pointer-chasing overhead |
96. Walk through profiling and fixing a real production incident where a service iterating a LinkedList<Order> via an index-based for loop caused a latency spike as the dataset grew.
The typical incident shape: the code was originally written and tested against a small, in-memory list of a few dozen orders, where an index-based for (int i = 0; i < orders.size(); i++) orders.get(i) loop performed indistinguishably from any alternative. As the dataset grew organically into the thousands or tens of thousands of orders, latency degraded non-linearly, since each get(i) call on a LinkedList walks from the head (or tail) every single time, turning the intended O(n) loop into O(n²) work overall.
A profiler flame graph or CPU sampling trace makes this immediately visible: an outsized fraction of total time attributes to LinkedList.node(), the internal pointer-walking method backing get(index), called far more times than the loop's iteration count would suggest for a healthy O(n) algorithm. The fix is simply to iterate with the enhanced for-loop (or an explicit Iterator), which walks node-to-node once in true O(n); as a regression guard, a code-review rule or static-analysis check flagging any get(index) call on a non-RandomAccess List reference catches the anti-pattern before it ships again.
97. Can a Java Iterator itself be meaningfully serialized so that iteration can be paused and resumed later (e.g., across JVM restarts)? Explain why this rarely works and what alternative achieves the same goal.
In practice, no. The JDK's own iterator implementations (such as ArrayList$Itr) are not designed to be serialized, hold a live back-reference to their exact parent collection instance plus internal bookkeeping like expectedModCount, and even a custom iterator that could technically be serialized would deserialize into a dangling reference unless the exact backing collection instance is also somehow reconstructed identically on the other side, which defeats the purpose for anything beyond a trivial in-memory toy case.
The alternative that actually achieves "pause and resume across process boundaries" is to persist a small, self-contained position marker, the last processed record's unique key or an offset, separately from any live iterator object, and on resume construct a brand-new iterator (or re-run a query) that starts strictly after that marker. This is the same keyset/cursor idea used for resumable API pagination, and it works precisely because it never tries to serialize live traversal state, only a durable position.
98. Explain the technique of returning a snapshot iterator, an iterator over a defensive copy taken at iterator() call time, and how it trades off against a live fail-fast iterator.
A snapshot iterator copies the entire current contents of the backing structure into an independent array or list at the exact moment iterator() is invoked, then iterates that private copy, completely detached from the live structure afterward. This guarantees a ConcurrentModificationException can never occur, since nothing about later mutation of the live structure can be observed by an iterator that no longer references it, and it also guarantees the caller sees one consistent, unchanging point-in-time view even if the live structure is being concurrently mutated throughout the iteration.
The trade-off is a straightforward cost-for-safety exchange: taking the snapshot costs O(n) time and O(n) extra memory on every single call to iterator(), and any writes that happen after the snapshot was taken are simply invisible to that iteration, which is sometimes exactly the desired "consistent view" semantics and sometimes an unwanted staleness, depending on the use case. This is precisely the mechanism CopyOnWriteArrayList bakes directly into the collection itself rather than leaving it to each caller to implement ad hoc.
99. Show how the Iterator pattern and the Builder pattern can be combined, for example an Iterator that lazily builds and yields fully-configured objects one at a time from a stream of raw input records.
This combination is useful whenever the raw input, a line from a file, a row from a network response, needs non-trivial multi-step assembly into a fully validated domain object before it can be handed to the caller, but you still want the overall traversal to stay lazy and one-at-a-time rather than eagerly building every object upfront. The iterator's next() pulls one raw record from an inner iterator, then drives a Builder through its construction steps internally before returning the finished, immutable result.
public class OrderRecordIterator implements Iterator<Order> {
private final Iterator<String> rawLines;
public OrderRecordIterator(Iterator<String> rawLines) { this.rawLines = rawLines; }
@Override public boolean hasNext() { return rawLines.hasNext(); }
@Override public Order next() {
String line = rawLines.next(); // e.g. "42,ACME,199.99,EXPRESS"
String[] fields = line.split(",");
return new Order.Builder()
.id(Long.parseLong(fields[0]))
.customer(fields[1])
.total(new BigDecimal(fields[2]))
.shipping(ShippingMethod.valueOf(fields[3]))
.build(); // Builder performs validation before returning
}
}
100. Give the definitive, interview-ready summary distinguishing Iterable, List, and Stream, addressing what each one is (or is not), and how they relate to the Iterator pattern.
Iterable<T> is a single-method contract, "I can produce an Iterator<T> on demand", and is what actually enables a type to be used in an enhanced for-each loop; it carries no data itself. List<T> is a concrete, stateful collection interface that stores an ordered sequence of elements, supports random access by index, and, among other things, implements Iterable<T> so it can be walked, but being iterable is only one of many things a List does. Stream<T> is neither: it is not a data structure and holds no elements of its own, it is a one-time-use, potentially lazy and parallel pipeline of operations to be run over a source; a stream can only be consumed once and is left unusable afterward, unlike a List, which can be iterated repeatedly forever.
A frequent interview trap worth naming explicitly: many candidates assume Stream must implement Iterable since it exposes an iterator() method, but it does not, Stream deliberately does not extend Iterable<T>, which is precisely why a Stream cannot be used directly in a for-each loop without first calling that iterator() method explicitly (or converting it to a List/array).
| Iterable | List | Stream | |
|---|---|---|---|
| Holds data? | No, just a contract | Yes | No, a pipeline |
| Reusable across multiple traversals? | Depends on implementer | Yes, always | No, single-use |
| Usable in for-each directly? | Yes, by definition | Yes (implements Iterable) | No (does not implement Iterable) |
Post a Comment
Add