Definition
- 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
ConcurrentModificationExceptionon 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.
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
}
}
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.
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);
}
}
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.
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());
}
}
}
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.
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);
}
}
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.
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);
}
}
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
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
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
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
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
// 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
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
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.
---