Why can a subList view throw ConcurrentModificationException, and how do you avoid it?
answer
- Structural = size change
- modCount vs expectedModCount
- Through-the-view is allowed
- Parent-direct change breaks it
- Fail-fast, not thread-related
basics
~20 sA subList view becomes invalid if you structurally change the original list (add or remove elements) through some other reference. The next time you use the view, it usually throws ConcurrentModificationException. Avoid it by using the view only within a short scope.
solid answer
~40 ssubList returns a view backed by the parent, and the contract says the view is valid only while the parent is not structurally modified except through the returned view. A structural modification is one that changes the parent's size: add, remove, or clear directly on the parent. When that happens, the view's cached size/expectations no longer match the parent's modCount, so the next operation on the view throws ConcurrentModificationException (fail-fast behavior). The safe pattern is to create the view, do your range work through it, and discard it before touching the parent directly. If you must mutate the parent's size, either do it through the view (sub.clear()) or take a copy first with new ArrayList<>(sub). Note the view itself may legally do structural changes to the parent, that is the supported channel.
code
java · 9 linesList<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> view = nums.subList(1, 4); // [2, 3, 4]
nums.add(99); // structural change to the PARENT, bypassing the view
view.get(0); // throws ConcurrentModificationException
// Safe alternatives:
// 1) mutate through the view: nums.subList(1, 4).clear();
// 2) snapshot: List<Integer> snap = new ArrayList<>(nums.subList(1, 4));go deeper
Knows that changing the original list while holding a subList can blow up, and that keeping the view short-lived avoids the problem.
Distinguishes structural from non-structural changes, knows mutating through the view is allowed, and can avoid CME by scoping or copying.
Explains the modCount/expectedModCount fail-fast mechanism, notes it is single-threaded-reproducible and best-effort, and chooses copy-vs-view deliberately.
Discusses fail-fast as a design principle (surface bugs early, no guarantee), its limits for concurrency, and how view lifetime should be designed into APIs and code review rules.
## Setup: view + backing list Recall that `subList(from, to)` returns a **view** — a wrapper holding a reference to the parent, the `fromIndex` offset, and a `size`. It shares the parent's element storage. ## Structural vs non-structural modification - A **structural modification** changes the **size** of the list: `add`, `remove`, `clear`, `removeIf`, etc. - A **non-structural modification** changes an existing element without changing size: `set(i, x)`. The subList contract states: *the semantics of the returned list become undefined if the backing list is structurally modified in any way other than via the returned list.* In practice the implementation makes this **fail-fast**. ## How fail-fast works (modCount) `AbstractList` keeps a counter called `modCount` that is incremented on every structural change. When you create a subList, the view records the parent's current `modCount` as its `expectedModCount`. Before each view operation, the view checks `parent.modCount == expectedModCount`. If you structurally modified the **parent directly** (not through the view), the counts diverge and the view throws `ConcurrentModificationException` (CME). When you modify through the **view**, the view updates its own `expectedModCount`, so the counts stay in sync and no exception is thrown. ## Worked failure ``` List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5)); List<Integer> view = nums.subList(1, 4); // [2,3,4] nums.add(99); // structural change to PARENT, not via view view.get(0); // throws ConcurrentModificationException ``` Note: this is *not* about threads. CME here is purely about modifying the parent through another path; it can happen single-threaded. ## Worked success (mutate through the view) ``` List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5)); nums.subList(1, 4).clear(); // structural change THROUGH the view — fine // nums is now [1, 5] ``` ## Avoidance patterns 1. **Short-scope discipline.** Create the view, use it, drop it, all before touching the parent directly. Do not hold a subList across code that mutates the parent. 2. **Mutate through the view** when you intend to change the parent's range. 3. **Copy when you need independence:** `List<Integer> snap = new ArrayList<>(nums.subList(1,4));` — now mutating `nums` cannot break `snap`. 4. **Beware nested subLists** — a subList of a subList compounds the same fragility. ## Why fail-fast at all? Without it, a stale view could silently read wrong elements or skip/duplicate during iteration. Fail-fast surfaces the bug immediately and loudly rather than corrupting data — but it is a best-effort debugging aid, not a guaranteed contract, so never rely on CME for program logic.
- Does calling parent.set(0, x) invalidate a subList view?No. set replaces an existing element without changing size, so it is non-structural and does not bump modCount. The view stays valid.
- Is ConcurrentModificationException guaranteed when you misuse a view?No. It is a best-effort fail-fast aid. The Javadoc says behavior is undefined; CME is thrown on a best-effort basis and must not be relied on for correctness.
saying these in an interview costs you the question
- Thinking CME only happens with multiple threads
- Believing set(i, x) on the parent invalidates the view (it does not — set is non-structural)
- Assuming you can never modify the parent while a subList exists (modifying THROUGH the view is fine)
- Relying on CME being guaranteed for program control flow