Back to List: ArrayList & LinkedList
Q7Question 7 of 7

What are the gotchas of List.subList()?

Key Concepts
  • subList() returns a view backed by the original list, not an independent copy; modifications to the sublist write through to the parent
  • Structural changes to the parent (add/remove) after creating the sublist invalidate it; the next sublist access throws ConcurrentModificationException
  • Non-structural changes to the parent (set) do not invalidate the sublist; they are visible through the view
  • For an independent copy, wrap in a new ArrayList: new ArrayList<>(list.subList(from, to))
  • subList is useful for efficient bulk operations: list.subList(from, to).clear() removes a range in one call
  • The sublist shares the parent's modCount; any structural modification increments modCount and breaks the sublist's expectation

Definition

subList(fromIndex, toIndex) returns a view into the parent list, not a copy; writes propagate both ways, and structural changes to the parent invalidate the sublist with ConcurrentModificationException.
  • View, not copy: list.subList(from, to) returns a lightweight wrapper that points to the parent's internal array (for ArrayList) at the specified range
  • Write-through: set(), add(), remove() on the sublist modify the parent list at the corresponding positions
  • Invalidation: any structural change to the parent (add, remove, clear) after creating the sublist causes ConcurrentModificationException on the next sublist operation
  • Independent copy: new ArrayList<>(list.subList(from, to)) creates a separate list with its own backing array
  • Range operations: subList(from, to).clear() efficiently removes a range of elements from the parent

Think of subList() as cutting a window in the bank ledger. You see rows 10 through 20 through the window, but it is still the same ledger. Write something on the window, and the ledger changes. Tear a page from the ledger (structural change), and the window breaks – you get a ConcurrentModificationException. To get a safe copy, photocopy pages 10 through 20 into a separate notebook.

Understanding the Concepts

What does subList return?

list.subList(fromIndex, toIndex) returns a List that is a view of the specified range of the parent list. The range is [fromIndex, toIndex) – fromIndex is inclusive, toIndex is exclusive.

The returned list is not a new collection with its own storage. It is a lightweight wrapper that delegates all operations to the parent list at the appropriate offset. For ArrayList, the sublist holds a reference to the parent's internal Object[] array and adjusts indices by the offset.

JAVA
package com.bank.demo;

import java.util.*;

public class SubListBasicsDemo {
    public static void main(String[] args) {
        List<String> accounts = new ArrayList<>(List.of(
            "SAV2001", "CUR3001", "FD4001", "LON5001", "SAV2002"
        ));

        // subList is a VIEW of indices 1 through 3
        List<String> sub = accounts.subList(1, 4);
        System.out.println("Parent: " + accounts);
        System.out.println("SubList: " + sub);
        System.out.println("SubList size: " + sub.size());

        // Accessing elements in the sublist
        System.out.println("sub.get(0): " + sub.get(0)); // CUR3001
        System.out.println("sub.get(2): " + sub.get(2)); // LON5001
    }
}
TEXT
Parent: [SAV2001, CUR3001, FD4001, LON5001, SAV2002]
SubList: [CUR3001, FD4001, LON5001]
SubList size: 3
sub.get(0): CUR3001
sub.get(2): LON5001

How do modifications propagate between sublist and parent?

All modifications to the sublist write through to the parent, and non-structural changes to the parent within the sublist's range are visible through the sublist.

JAVA
package com.bank.demo;

import java.util.*;

public class SubListWriteThroughDemo {
    public static void main(String[] args) {
        List<String> accounts = new ArrayList<>(List.of(
            "SAV2001", "CUR3001", "FD4001", "LON5001", "SAV2002"
        ));
        List<String> sub = accounts.subList(1, 4);

        // Modify through sublist → parent changes
        sub.set(0, "CUR3001-UPDATED");
        System.out.println("After sub.set(0): " + accounts);
        // Parent's index 1 changed!

        // Remove through sublist → parent changes
        sub.remove(0);
        System.out.println("After sub.remove(0): " + accounts);
        System.out.println("SubList now: " + sub);

        // Add through sublist → parent changes
        sub.add(0, "NEW-ACC");
        System.out.println("After sub.add: " + accounts);
    }
}
TEXT
After sub.set(0): [SAV2001, CUR3001-UPDATED, FD4001, LON5001, SAV2002]
After sub.remove(0): [SAV2001, FD4001, LON5001, SAV2002]
SubList now: [FD4001, LON5001]
After sub.add: [SAV2001, NEW-ACC, FD4001, LON5001, SAV2002]

When does ConcurrentModificationException occur?

If you make a structural change to the parent list (add, remove, clear) after creating the sublist, the sublist becomes invalid. The next operation on the sublist throws ConcurrentModificationException.

The mechanism: the sublist stores the parent's modCount at creation time. Each structural change to the parent increments modCount. When you access the sublist, it checks if the parent's modCount still matches the stored value. If not, it throws CME.

Non-structural changes to the parent (like set()) do not increment modCount and do not invalidate the sublist.

JAVA
package com.bank.demo;

import java.util.*;

public class SubListInvalidationDemo {
    public static void main(String[] args) {
        List<String> accounts = new ArrayList<>(List.of(
            "SAV2001", "CUR3001", "FD4001", "LON5001"
        ));
        List<String> sub = accounts.subList(1, 3); // [CUR3001, FD4001]

        // Non-structural change to parent – sublist is fine
        accounts.set(0, "SAV2001-RENAMED");
        System.out.println("After parent set: " + sub); // still works

        // Structural change to parent – sublist INVALIDATED
        accounts.add("NEW-ACC"); // modCount incremented

        try {
            sub.get(0); // throws ConcurrentModificationException!
        } catch (ConcurrentModificationException e) {
            System.out.println("CME on get: " + e.getClass().getSimpleName());
        }

        try {
            sub.size(); // also throws!
        } catch (ConcurrentModificationException e) {
            System.out.println("CME on size: " + e.getClass().getSimpleName());
        }
    }
}
TEXT
After parent set: [CUR3001, FD4001]
CME on get: ConcurrentModificationException
CME on size: ConcurrentModificationException

How do you create an independent copy?

Wrap the sublist in a new ArrayList constructor. This creates a separate list with its own backing array. Changes to either the original or the copy do not affect the other.

JAVA
package com.bank.service;

import java.util.*;

public class IndependentCopyDemo {
    public static void main(String[] args) {
        List<String> allTransactions = new ArrayList<>(List.of(
            "TXN-001", "TXN-002", "TXN-003", "TXN-004", "TXN-005"
        ));

        // Independent copy for pagination
        List<String> page1 = new ArrayList<>(allTransactions.subList(0, 3));
        List<String> page2 = new ArrayList<>(allTransactions.subList(3, 5));

        // Modify original – copies are unaffected
        allTransactions.add("TXN-006");
        allTransactions.remove(0);

        System.out.println("Original: " + allTransactions);
        System.out.println("Page 1 (copy): " + page1); // unchanged
        System.out.println("Page 2 (copy): " + page2); // unchanged

        // Modify copy – original is unaffected
        page1.add("EXTRA");
        System.out.println("Page 1 modified: " + page1);
        System.out.println("Original still: " + allTransactions);
    }
}
TEXT
Original: [TXN-002, TXN-003, TXN-004, TXN-005, TXN-006]
Page 1 (copy): [TXN-001, TXN-002, TXN-003]
Page 2 (copy): [TXN-004, TXN-005]
Page 1 modified: [TXN-001, TXN-002, TXN-003, EXTRA]
Original still: [TXN-002, TXN-003, TXN-004, TXN-005, TXN-006]

How do you use subList for efficient bulk operations?

The view nature of subList makes it perfect for bulk operations. Since the sublist delegates to the parent, clear(), sort(), replaceAll(), and removeIf() on the sublist operate on the parent's range directly.

JAVA
package com.bank.demo;

import java.util.*;

public class SubListBulkOpsDemo {
    public static void main(String[] args) {
        // Efficient range delete
        List<String> transactions = new ArrayList<>(List.of(
            "TXN-001", "TXN-002", "TXN-003", "TXN-004", "TXN-005", "TXN-006"
        ));
        System.out.println("Before clear: " + transactions);
        transactions.subList(2, 5).clear(); // remove indices 2, 3, 4
        System.out.println("After clear(2,5): " + transactions);

        // Sort a sub-range
        List<String> accounts = new ArrayList<>(List.of(
            "VIP1", "VIP2", "FD4001", "SAV2001", "CUR3001", "REG1", "REG2"
        ));
        // Sort only the middle section (indices 2-5), leave VIP and REG untouched
        accounts.subList(2, 5).sort(Comparator.naturalOrder());
        System.out.println("Partial sort: " + accounts);

        // removeIf on a range
        List<String> data = new ArrayList<>(List.of(
            "KEEP", "KEEP", "REMOVE-A", "KEEP", "REMOVE-B", "KEEP"
        ));
        data.subList(2, 5).removeIf(s -> s.startsWith("REMOVE"));
        System.out.println("After removeIf: " + data);
    }
}
TEXT
Before clear: [TXN-001, TXN-002, TXN-003, TXN-004, TXN-005, TXN-006]
After clear(2,5): [TXN-001, TXN-002, TXN-006]
Partial sort: [VIP1, VIP2, CUR3001, FD4001, SAV2001, REG1, REG2]
After removeIf: [KEEP, KEEP, KEEP, KEEP]

What are the edge cases?

Empty sublist

JAVA
List<String> list = List.of("A", "B", "C");
List<String> empty = list.subList(1, 1); // fromIndex == toIndex
System.out.println("Empty sublist size: " + empty.size()); // 0

subList of a subList

JAVA
List<String> list = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
List<String> sub1 = list.subList(1, 4); // [B, C, D]
List<String> sub2 = sub1.subList(0, 2); // [B, C] – view of a view
sub2.set(0, "B2");
System.out.println("Original: " + list); // [A, B2, C, D, E]
// Modifying sub2 affects sub1 and the original

IndexOutOfBoundsException

JAVA
List<String> list = List.of("A", "B", "C");
try {
    list.subList(1, 5); // toIndex > size
} catch (IndexOutOfBoundsException e) {
    System.out.println("Out of bounds: " + e.getMessage());
}
try {
    list.subList(2, 1); // fromIndex > toIndex
} catch (IllegalArgumentException e) {
    System.out.println("Invalid range: " + e.getMessage());
}

What are common mistakes?

Mistake 1: Caching a sublist and modifying the parent later

JAVA
List<String> master = new ArrayList<>(List.of("A", "B", "C", "D"));
List<String> cached = master.subList(0, 2); // cached view

// Later in the code...
master.add("E"); // structural modification

// Much later...
cached.get(0); // ConcurrentModificationException!
// Fix: cache an independent copy instead
List<String> safeCopy = new ArrayList<>(master.subList(0, 2));

Mistake 2: Returning a sublist from a method

JAVA
// WRONG – caller gets a view that may be invalidated
public List<String> getPage(int offset, int limit) {
    return transactions.subList(offset, offset + limit); // view!
}
// If transactions is modified later, the returned view breaks

// CORRECT – return an independent copy
public List<String> getPage(int offset, int limit) {
    return new ArrayList<>(transactions.subList(offset, Math.min(offset + limit, transactions.size())));
}

Mistake 3: Using subList with immutable lists

JAVA
List<String> immutable = List.of("A", "B", "C");
List<String> sub = immutable.subList(0, 2);
try {
    sub.set(0, "X"); // UnsupportedOperationException – immutable!
} catch (UnsupportedOperationException e) {
    System.out.println("Cannot modify immutable sublist");
}
// The sublist inherits the immutability of the parent

What about thread safety?

Sublists are not thread-safe, even if the parent is wrapped with Collections.synchronizedList(). The synchronized wrapper synchronizes operations on the parent, but the sublist bypasses the wrapper and accesses the underlying list directly. For thread-safe pagination, create an independent copy inside a synchronized block.

What should a senior engineer know?

- Memory efficiency. SubList does not copy data, so creating a sublist of a 10-million-element list is O(1) and uses no extra memory. This makes it ideal for passing a range to a method that reads but does not modify.

- SubList prevents GC of the parent. If you hold a reference to a sublist, the entire parent list stays in memory (because the sublist references the parent's array). If you want just a small slice and want the rest garbage-collected, make an independent copy.

- AbstractList implementation. The sublist returned by ArrayList is an inner class (SubList) that extends AbstractList. It stores the parent reference, offset, and size. All operations delegate to the parent with index adjustment.

- Unmodifiable sublists. List.of(...).subList(from, to) returns an unmodifiable sublist. Collections.unmodifiableList(list).subList(from, to) also returns an unmodifiable sublist. Both throw UnsupportedOperationException on write operations.

- Streams as alternative. For read-only range processing, list.stream().skip(from).limit(to - from) avoids the view/invalidation issues of subList, but creates a stream pipeline overhead.

Visual Overview

DIAGRAM
  subList() = VIEW (backed by parent)

  Parent: [SAV2001, CUR3001, FD4001, LON5001, SAV2002]
                     ├──── subList(1,4) ────┤
  SubList:          [CUR3001, FD4001, LON5001]

  sub.set(0, "X") → parent[1] = "X"   (write-through)
  parent.add("Z") → subList INVALID    (ConcurrentModificationException)

  Independent copy:
  new ArrayList<>(parent.subList(1,4))
  ┌──────────────────┐   ┌──────────────────┐
  │     Parent       │   │      Copy        │
  │ [..., CUR, ...]  │   │ [CUR, FD, LON]   │
  │                  │   │ (own backing     │
  │ parent.add("Z") │   │  array – safe)   │
  │ → copy unaffected│   └──────────────────┘
  └──────────────────┘

Key Takeaway

SubList is one of the most misunderstood features in Java collections. It returns a view – a lightweight window into the parent list – not an independent copy. This makes it efficient for read-only operations and bulk modifications like subList(from, to).clear(), but dangerous if you cache it or return it from a method.

The trap is that any structural change to the parent (add, remove, clear) after creating the sublist invalidates it. The next access throws ConcurrentModificationException. This catches many developers off guard, especially in banking systems where a service caches a sublist for pagination and the underlying transaction list grows with new entries.

The fix is simple: wrap in new ArrayList<>(list.subList(from, to)) when you need a safe, independent copy. This creates a separate list with its own backing array. Changes to the original do not affect the copy, and vice versa.

In Rajesh's banking system, the transaction history API returns paginated results using new ArrayList<>(transactions.subList(offset, offset + pageSize)). The copy ensures that new transactions arriving during the API response do not invalidate the page.

Remember one sentence: subList is a view that writes through and breaks on parent structural changes – always copy it if you are returning it or caching it.

---

Share
Use ← → arrow keys to navigate