Understanding Java ArrayList subList() Pitfalls and Safe Usage

Memory Leak Caused by subList

A common mistake with subList() is inadvertently retaining a reference to the original large list, leading to an OutOfMemoryError. Consider this example:

@Slf4j
public class SubListDemo {

    public static void subListOOM() {
        List<List<Integer>> data = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            // Create a list with 100,000 elements
            List<Integer> rawList = IntStream.rangeClosed(1, 100000)
                                           .boxed()
                                           .collect(Collectors.toList());
            data.add(rawList.subList(0, 1));
        }
        log.info("data.size(): " + data.size());
    }
}

Running this method triggers:

Exception in thread "restartedMain" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.lang.Integer.valueOf(Integer.java:832)
    ...

The root cause lies in the implementation of subList():

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

The returned SubList holds a strong reference to the original list via its parent field. It does not create a copy of the elements; it is merely a view. Therefore, keeping many SubList instances prevents the original large lists from being garbage collected, causing OOM.

Solution: Wrap the sublist in a new ArrayList to obtain an independent copy:

public static void subListWithoutOOM() {
    List<List<Integer>> data = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
        List<Integer> rawList = IntStream.rangeClosed(1, 100000)
                                       .boxed()
                                       .collect(Collectors.toList());
        data.add(new ArrayList<>(rawList.subList(0, 1)));
    }
    log.info("data.size(): " + data.size());
}

Now once the loop iteration finishes, rawList becomes eligible for garbage collection becuase the SubList is not retained.

Side Effects on the Original List

Because subList() returns a view, modifications to the sublist directly affect the original list. For example:

public static void removeSubList() {
    List<Integer> rawList = IntStream.rangeClosed(1, 10)
                                   .boxed()
                                   .collect(Collectors.toList());
    List<Integer> subList = rawList.subList(0, 3);
    subList.remove(0);
    rawList.forEach(System.out::print);
}
// Output: 2345678910

Removing the first element from subList also removes it from rawList.

ConcurrentModificationException When Modifying the Original List

Structural modifications to the original list after creating a SubList invalidate the sublist, leading to a ConcurrentModificationException.

public static void addItemToOriginalList() {
    List<Integer> rawList = IntStream.rangeClosed(1, 10)
                                   .boxed()
                                   .collect(Collectors.toList());
    List<Integer> subList = rawList.subList(0, 3);

    rawList.add(11);
    try {
        subList.forEach(System.out::print);
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

This throws:

java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
    ...

Internaly, ArrayList tracks structural modifications (e.g., add, remove) with modCount. When the original list's modCount changes, the sublist’s modCount becomes stale, and the sublist’s methods (like iterator()) call checkForComodification():

private void checkForComodification() {
    if (ArrayList.this.modCount != this.modCount)
        throw new ConcurrentModificationException();
}

Best Practices

  • Independence: If you need a separate list, always copy the subList() result: new ArrayList<>(original.subList(...)).
  • No Side Effects: Avoid modifying the original list while a sublist is in use, or ensure the sublist is discarded after such modifications.
  • Resource Management: For long-lived sublists that reference large parent lists, copy them to prevent memory leaks.

Tags: java ArrayList subList Memory Leak ConcurrentModificationException

Posted on Sat, 09 May 2026 05:39:15 +0000 by jstone3503