Fundamentals 20 min read

Common Pitfalls When Using Java List Implementations and How to Avoid Them

This article systematically examines ten typical pitfalls encountered when converting arrays to lists, performing add/remove operations, using subList, handling memory consumption, and working with thread‑safe collections such as CopyOnWriteArrayList in Java, and provides concrete code‑level solutions and performance recommendations.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Common Pitfalls When Using Java List Implementations and How to Avoid Them

1. Arrays.asList with Primitive Arrays

When converting a primitive array to a List using Arrays.asList , the resulting list contains a single element because the whole primitive array is treated as one object.

int[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
System.out.println(list.size()); // 1

Solution 1: Use Java 8 streams to box the primitives.

List
collect = Arrays.stream(arr).boxed().collect(Collectors.toList());
System.out.println(collect.size()); // 3
System.out.println(collect.get(0).getClass()); // class java.lang.Integer

Solution 2: Declare the array with wrapper types.

Integer[] integerArr = {1, 2, 3};
List
integerList = Arrays.asList(integerArr);
System.out.println(integerList.size()); // 3
System.out.println(integerList.get(0).getClass()); // class java.lang.Integer

2. Immutable List Returned by Arrays.asList

The list returned by Arrays.asList does not support structural modifications because it is backed by an internal ArrayList class that extends AbstractList without implementing add and remove .

private static void asListAdd() {
    String[] arr = {"1", "2", "3"};
    List
strings = new ArrayList<>(Arrays.asList(arr));
    arr[2] = "4";
    System.out.println(strings.toString());
    Iterator
iterator = strings.iterator();
    while (iterator.hasNext()) {
        if ("4".equals(iterator.next())) {
            iterator.remove();
        }
    }
    strings.forEach(val -> {
        strings.remove("4");
        strings.add("3");
    });
    System.out.println(Arrays.asList(arr).toString());
}
// Throws UnsupportedOperationException

Wrap the result in a new ArrayList to obtain a mutable list.

List
mutable = new ArrayList<>(Arrays.asList(arr));

3. Modifying the Original Array Affects the List

The list created by Arrays.asList holds a reference to the original array, so any change to the array is reflected in the list.

public static
List
asList(T... a) {
    return new ArrayList<>(a);
}
ArrayList(E[] array) { a = Objects.requireNonNull(array); }

Solution: Create a new ArrayList from the returned list to break the reference.

List
independent = new ArrayList<>(Arrays.asList(arr));

4. java.util.ArrayList May Still Appear Immutable

Even when wrapping the Arrays.asList result with new ArrayList<>(...) , the underlying list may still throw UnsupportedOperationException if the internal implementation does not support add / remove . The root cause is the same internal ArrayList class that lacks those methods.

5. SubList Cast to ArrayList Causes ClassCastException

Calling list.subList(...) returns an internal SubList view, not a true ArrayList . Casting it to ArrayList results in a ClassCastException .

List
names = new ArrayList
() {{ add("one"); add("two"); add("three"); }};
ArrayList strings = (ArrayList) names.subList(0, 1); // throws

Solution: Use a new ArrayList to copy the sub‑list.

List
copy = new ArrayList<>(names.subList(0, 1));

6. SubList Can Lead to OOM

Because a sub‑list holds a reference to the original large list, repeatedly creating sub‑lists in a loop prevents the original list from being garbage‑collected, eventually causing OutOfMemoryError .

private static void subListOomTest() {
    IntStream.range(0, 1000).forEach(i -> {
        List
collect = IntStream.range(0, 100000).boxed().collect(Collectors.toList());
        data.add(collect.subList(0, 1));
    });
    // OOM

Solution: Copy the sub‑list into a new container or use stream skip / limit for slicing.

List
list = new ArrayList<>(collect.subList(0, 1));
List
list2 = collect.stream().skip(0).limit(1).collect(Collectors.toList());

7. LinkedList Insertion Not Always Faster Than ArrayList

Performance tests inserting 100 000 random elements show that LinkedList can be significantly slower than ArrayList because each insertion requires node traversal.

private static void test() {
    StopWatch sw = new StopWatch();
    int elementCount = 100000;
    sw.start("ArrayList add");
    List
arrayList = IntStream.rangeClosed(1, elementCount).boxed()
        .collect(Collectors.toCollection(ArrayList::new));
    IntStream.rangeClosed(0, elementCount).forEach(i ->
        arrayList.add(ThreadLocalRandom.current().nextInt(elementCount), 1));
    sw.stop();
    sw.start("LinkedList add");
    List
linkedList = IntStream.rangeClosed(1, elementCount).boxed()
        .collect(Collectors.toCollection(LinkedList::new));
    IntStream.rangeClosed(0, elementCount).forEach(i ->
        linkedList.add(ThreadLocalRandom.current().nextInt(elementCount), 1));
    sw.stop();
    System.out.println(sw.prettyPrint());
}

Conclusion: Use LinkedList only for head/tail operations; otherwise prefer ArrayList after benchmarking.

8. CopyOnWriteArrayList High Memory Overhead

Each write creates a fresh copy of the internal array, doubling memory usage and causing frequent GC, especially with large collections.

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

9. CopyOnWriteArrayList Iterator Weak Consistency

The iterator works on a snapshot taken at creation time; modifications after that are invisible.

public Iterator
iterator() {
    return new COWIterator
(getArray(), 0);
}
static final class COWIterator
implements ListIterator
{
    private final Object[] snapshot;
    private int cursor;
    // ... hasNext, next, etc.
}

Demo shows that changes made in another thread are not reflected when iterating over the original iterator.

10. CopyOnWriteArrayList Iterator Does Not Support Modification

The iterator’s remove , add and set methods always throw UnsupportedOperationException because the iterator traverses a read‑only snapshot.

public void remove() { throw new UnsupportedOperationException(); }
public void add(E e) { throw new UnsupportedOperationException(); }
public void set(E e) { throw new UnsupportedOperationException(); }

Summary

The article highlights frequent pitfalls when using Java collection utilities such as Arrays.asList , ArrayList , LinkedList , subList , and CopyOnWriteArrayList . Understanding these behaviors helps developers choose the right data structure, avoid hidden bugs, memory leaks, and performance regressions.

JavaPerformanceConcurrencylistArrayListcopyonwritearraylistLinkedList
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.