You cancel inactive orders from a bag that might be an ArrayList today and a HashSet tomorrow. The loop, the remove, and the bulk filter do not care which class you new. They care that the bag is a Collection you can iterate and mutate under one contract.
Collection is that contract. Iterator is how you walk it without breaking the walk.
The Collections Roadmap owns the glossary this series will not re-teach: Collection vs Map, optional operations, fail-fast vs weakly consistent vs snapshot, views vs copies. This post is the methods every List and Set already inherited — and the iterator rules that make a remove during a walk legal or a ConcurrentModificationException.
Program to Collection when any bag of elements will do. Walk it with Iterator when the walk itself must mutate.
Iterable is not enough
for-each needs Iterable. That is only iterator(). Collection adds size, membership, optional mutation, bulk methods, and a copy out to an array.
Iterable<Order> iterable = orders;
for (Order order : iterable) {
System.out.println(order.id());
}
Collection<Order> bag = orders;
bag.size();
bag.contains(order);
bag.add(order);
You can for-each a list or a set because both are collections, and a collection is iterable. You cannot for-each a Map; you iterate a view. That split lives on the roadmap. This post does not re-draw it.
Collection is the shared surface of List and Set. The class you new is how the JDK delivers it.
The contract
Trimmed to the methods this post uses:
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
boolean add(E e);
boolean remove(Object o);
void clear();
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean removeIf(Predicate<? super E> filter);
Stream<E> stream();
Stream<E> parallelStream();
}
add returns true when the collection changed. A List that accepts the element almost always returns true. A Set returns false on a duplicate. That boolean is part of the contract, not a HashSet quirk.
| Method | Job |
|---|---|
size() / isEmpty() | How many elements, or none |
contains(o) | Membership by equals |
add(e) / remove(o) / clear() | Optional mutation |
iterator() | Fail-fast walk for java.util types |
toArray | Snapshot into an array (a copy) |
containsAll / addAll / removeAll / retainAll | Bulk membership and set-algebra |
removeIf(filter) | Drop matching elements through the iterator |
stream() / parallelStream() | On-ramp to a pipeline |
contains uses equals, not ==. Two Order records with the same components are the same member even if they are different objects.
Collection<Order> open = new ArrayList<>();
Order placed = new Order("o-1", "a@shop.test", List.of(), BigDecimal.ZERO, true);
open.add(placed);
boolean found = open.contains(
new Order("o-1", "a@shop.test", List.of(), BigDecimal.ZERO, true));
// found == true — record equality, not identity
Optional operations are already in your way
Collection.add is allowed to throw UnsupportedOperationException. That is the optional-operations rule the roadmap named. You hit it the first time you treat List.of like an ArrayList.
Collection<Order> frozen = List.of(placed);
frozen.add(placed); // UnsupportedOperationException
frozen.clear(); // UnsupportedOperationException
The interface is wider than any one implementation. Catching UnsupportedOperationException as control flow is a bug — pick a type that supports the mutation you need. new ArrayList<>(List.of(placed)) is a mutable copy. List.of is not.
size, isEmpty, contains, and iterator are the methods you can rely on. Mutation is the part that is optional.
Walk it: for-each, Iterator, remove
Enhanced-for is iterator sugar. The compiler calls iterator(), then hasNext / next until the walk ends.
for (Order order : orders) {
System.out.println(order.id());
}
That is the same walk as this, except you do not hold the Iterator:
Iterator<Order> it = orders.iterator();
while (it.hasNext()) {
Order order = it.next();
System.out.println(order.id());
}
You need the Iterator when the walk itself must remove. Iterator.remove deletes the last element next returned and updates the iterator’s expected modification count so the walk stays legal.
Iterator<Order> it = orders.iterator();
while (it.hasNext()) {
if (!it.next().active()) {
it.remove();
}
}
Enhanced-for cannot do that. There is no it in scope. Calling orders.remove(order) inside the loop is a structural change the iterator did not make:
for (Order order : orders) {
if (!order.active()) {
orders.remove(order); // ConcurrentModificationException
}
}
Note: remove() without a preceding next() throws IllegalStateException. Calling remove() twice in a row without another next() does too. The iterator removes the last returned element, not “whatever is next.”
Fail-fast is not thread-safety
Most java.util collections use a fail-fast iterator: a structural change during iteration, except Iterator.remove, throws ConcurrentModificationException. The roadmap owns fail-fast vs weakly consistent vs snapshot. The fact that changes a decision here is simpler:
orders.add(placed);
Iterator<Order> it = orders.iterator();
orders.add(another); // structural change
it.next(); // ConcurrentModificationException
You do not need a second thread. A single loop that mutates the collection through anything other than iterator.remove is enough. Fail-fast is a bug detector for that mistake. It is not a memory barrier, a lock, or a promise that two threads may share the list.
The Javadoc calls fail-fast best-effort. Do not write catch (ConcurrentModificationException) as business logic. Concurrent types (ConcurrentHashMap, CopyOnWriteArrayList) use weakly consistent or snapshot iterators and will not throw CME. Those contracts stay on the roadmap.
removeIf is the JDK’s bulk form of the iterator-remove loop:
orders.removeIf(order -> !order.active());
Same idea: the collection removes through its own iterator (or an equivalent internal walk). You do not hold a stale cursor.
Bulk methods
containsAll, addAll, removeAll, and retainAll are set algebra on any Collection. They are not Set-only.
Collection<String> basket = new ArrayList<>();
for (LineItem item : order.items()) {
basket.add(item.sku());
}
Collection<String> inStock = Set.of("SKU-A", "SKU-C");
basket.retainAll(inStock); // keep SKUs we can fulfill
basket.removeAll(Set.of("SKU-X"));
boolean allKnown = basket.containsAll(List.of("SKU-A"));
addAll is “union into this.” removeAll is “difference.” retainAll is “intersection, in place.” Each returns true if this changed.
Note: retainAll / removeAll call contains on the argument for each of your elements (or the reverse, depending on the implementation). If the argument is an ArrayList, each contains is a scan. If it is a HashSet, each contains is a hash lookup. The bulk method’s cost follows the argument’s membership cost — pass a Set when the other side is large.
ListIterator is the list-only extra
Iterator walks forward and can remove. ListIterator is the List-only extra: backward, set, and add at the cursor.
public interface ListIterator<E> extends Iterator<E> {
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void set(E e);
void add(E e);
}
Use it when you are already on a List and the walk must rewrite or insert, not just drop.
List<LineItem> items = new ArrayList<>(order.items());
ListIterator<LineItem> it = items.listIterator();
while (it.hasNext()) {
LineItem item = it.next();
if (item.sku().equals("SKU-OLD")) {
it.set(new LineItem("SKU-NEW", item.quantity(), item.unitPrice()));
}
if (item.quantity() > 10) {
it.add(new LineItem("SKU-GIFT", 1, BigDecimal.ZERO));
}
}
set replaces the last element next or previous returned. add inserts at the cursor (before the element that next would return). A Set has no ListIterator because a Set has no index and no “here, between these two.” The List post owns the rest of indexing.
toArray, stream, equals
toArray copies. The iterator does not. If you need a snapshot the next add cannot disturb, copy out:
Order[] snapshot = orders.toArray(Order[]::new); // Java 11
Order[] older = orders.toArray(new Order[0]);
Collection.stream() and parallelStream() are the on-ramp to a pipeline. The pipeline itself is Streams. Do not treat stream() as a second iterator API in this post — and do not reach for parallelStream() because it looks faster. The streams post owns map / filter; here the method exists so a Collection can become a Stream without you writing an adapter.
Collection itself does not specify equals. List and Set do, and they disagree on purpose.
List<String> listA = List.of("a", "b");
List<String> listB = new ArrayList<>(List.of("a", "b"));
listA.equals(listB); // true — same elements, same order
Set<String> setA = Set.of("a", "b");
Set<String> setB = new LinkedHashSet<>(List.of("b", "a"));
setA.equals(setB); // true — same members, order ignored
List.of("a", "b").equals(List.of("b", "a")); // false
Set.of("a", "b").equals(Set.of("b", "a")); // true
List.of("a").equals(Set.of("a")); // false — List vs Set
List equality is sequence. Set equality is membership. Two collections with the same elements are not equal across those contracts. hashCode follows the same split: a list hashes the ordered sequence; a set hashes the members.
Internals that change a decision
The interface does not promise Big-O. contains on an ArrayList scans. contains on a HashSet hashes. Both are legal Collection.contains. If the hot operation is “is this SKU already in the bag?”, the type that compiles (List) is the wrong delivery.
Fail-fast is a modCount on the collection and an expectedModCount on the iterator. Iterator.remove increments both. orders.add increments only the collection. The next next() notices the mismatch and throws. That is why the iterator’s own remove is legal and everyone else’s mutation is not.
toArray allocates. iterator() does not. Do not snapshot “just in case” on a hot path; do snapshot when you must iterate a stable picture while another method mutates the live collection on the same thread.
When Collection is the wrong type
Program to Collection on a parameter when any bag of elements is enough: “here are some orders; count them, filter them, copy them.” Reach past it when the job is more specific.
- Need
get(i),subList, or encounter-by-index →List. - Need uniqueness without scanning →
Set. - Need insert-at-end / take-from-front → Queue / Deque (next after Set).
- Need lookup by id →
Map. A map is not aCollection; its views are.
Do not use Collection as a way to hide that the caller must index. Do not use an ArrayList as a Collection whose only real operation is contains on thousands of SKUs.
Interview lens
Interviewers want the shared contract, not twenty class names. Draw Iterable → Collection → List / Set / Queue. Put Iterator.remove on the walk. Put Map off to the side.
Whiteboard one-liner. Fail-fast means structural change during iteration throws CME, except Iterator.remove. It is not thread-safety. Optional ops may throw UnsupportedOperationException.
| Operation | ArrayList | HashSet |
|---|---|---|
size / isEmpty | O(1) | O(1) |
contains / remove(Object) | O(n) | expected O(1) |
add | amortized O(1) | expected O(1) |
| iterator step | O(1) | O(1) |
retainAll(other) | follows other.contains | follows other.contains |
| Question | Honest answer |
|---|---|
| What does fail-fast actually mean? | Structural change during iteration → ConcurrentModificationException, except Iterator.remove. Best-effort bug detector. Not a concurrency guarantee. |
Why is iterator.remove allowed? | It is the iterator that tracks expectedModCount. It updates both counts. Any other structural change does not. |
| Why can’t enhanced-for remove? | The compiler holds the Iterator. You don’t. collection.remove inside the loop is a structural change that iterator did not make. |
| What is an optional operation? | add / remove / clear may throw UnsupportedOperationException. List.of does. The interface is wider than the implementation. |
Does Collection.equals care about order? | Collection itself does not specify equals. List.equals is order + elements. Set.equals is membership. A list never equals a set. |
Why does enhanced-for use Iterator? | Iterable.iterator() is the only walk for-each has. That is why CME shows up in a loop that never mentions Iterator. |
Wrong answer: “fail-fast means the collection is thread-safe.” Fail-fast is a single-threaded iterator contract. Sharing still needs a concurrent type, confinement, or a lock you document.
Cheat sheet
Iterable iterator() — enough for for-each
Collection size, contains, optional add/remove/clear, bulk, toArray, stream
add returns true if this collection changed (Set: false on duplicate)
Optional ops UOE on List.of / unmodifiable wrappers — pick a mutable type
Iterator hasNext / next / remove (remove last returned element)
for-each sugar over iterator(); no remove
Fail-fast CME on structural change except iterator.remove (java.util)
removeIf bulk form of the iterator-remove loop
ListIterator list-only: previous, set, add
toArray copy; iterator is not
equals List: order; Set: membership; List never equals Set
stream() on-ramp; Streams post owns the pipeline
Do Collection as a parameter when any bag will do
iterator.remove / removeIf to drop during a walk
Don't orders.remove inside for-each
catch CME or UOE as control flow
ArrayList.contains as uniqueness
Do:
- Type parameters and fields as
Collectionwhen indexing and uniqueness are not the job. - Remove during a walk with
Iterator.removeorremoveIf. - Pass a
SetintoretainAll/removeAllwhen the other side is large.
Don’t:
- Call
collection.removeinside enhanced-for. - Treat fail-fast as thread-safety.
- Invent a custom
Collectionto learn the contract — useArrayListandHashSet.
Wrap-up
Collection is the bag every List and Set already is: size, membership, optional mutation, bulk algebra, a copy out, and an iterator. Enhanced-for is that iterator. iterator.remove and removeIf are the legal ways to drop elements during a walk. Fail-fast catches the illegal ways; it does not make the bag safe to share.
Once you need an index, you are no longer in this post. That extra is List.