Arrays.asList() vs List.of() in Java: Why They Behave Differently
Arrays (java.util.Arrays) has been part of Java since 1.2. List (java.util.List) is just as old, but List.of() is a much newer addition — it arrived in Java 9. Both are one-line shortcuts for creating a List, and they look interchangeable at the call site. They are not. The objects they return come from different classes and play by different mutability rules. This post walks through the differences, shows what goes wrong if you pick the wrong one, and explains the internals that cause the split.
Key differences at a glance
| Method | get(int index) | set(int, T obj) | add(T obj) | remove(int index) |
|---|---|---|---|---|
Arrays.asList() | ✅ | ✅ | ❌ | ❌ |
List.of() | ✅ | ❌ | ❌ | ❌ |
The List returned by Arrays.asList() is actually java.util.Arrays$ArrayList — a private List implementation that lives inside Arrays, not the familiar java.util.ArrayList. It supports set(), but rejects add() and remove(). Calling either on an Arrays.asList() result throws java.lang.UnsupportedOperationException.
Arrays.asList()
A minimal example:
var a = Arrays.asList(0);
System.out.println(a);
// [0]
a.set(0, 1);
System.out.println(a);
// [1]
a.add(2);
// java.lang.UnsupportedOperationException
One subtlety worth knowing: the List returned by Arrays.asList() is backed by the same array you passed in — not a copy. The example below makes this concrete:
var arr = new Integer[] {5, 5, 5};
var list = Arrays.<Integer>asList(arr);
// check arr[1]
System.out.println(arr[1]); // 5
// modify through the list
list.set(1, 8);
// check arr[1] again
System.out.println(arr[1]); // 8
This is because Arrays.asList() is essentially just a thin wrapper around the array you hand it. The “Behind the scenes” section below goes deeper.
List.of()
The object returned by List.of() comes from a private subclass of java.util.ImmutableCollections. An empty list is java.util.ImmutableCollections$ListN; lists of one or two elements are java.util.ImmutableCollections$List12.
This is a read-only List, so any method that would modify it — set(), add(), remove() — throws java.lang.UnsupportedOperationException.
var a = List.of(0);
System.out.println(a);
// [0]
a.set(0, 1);
// java.lang.UnsupportedOperationException
If you need a list you can modify, you have to convert it explicitly.
Building a mutable List from List.of()
Wrap it in a real ArrayList (or LinkedList, or whatever list implementation fits the job). The extra copy looks like ceremony, but it guarantees the original immutable list stays untouched — which is the point.
var a = new ArrayList<Integer>(List.of(1, 2));
System.out.println(a);
// [1, 2]
a.add(3); // true ← returns success
System.out.println(a);
// [1, 2, 3]
a.remove(2); // 3 ← returns the removed element
System.out.println(a);
// [1, 2]
a.set(0, 5); // 1 ← returns the replaced element
System.out.println(a);
// [5, 2]
Behind the scenes
What is an ArrayList?
This isn’t specific to Java. Computer science has two foundational list structures: linked list and array list (often just called a dynamic array).
Linked list stores elements wherever memory happens to be free — they don’t need to be contiguous. Each element records its value plus a pointer to the next element. Three advantages come out of this: the list can grow as large as needed without pre-allocating, it tolerates fragmented memory, and inserting or deleting anywhere in the middle is cheap — you just repoint a pointer (understanding this is easier if you have some C-level mental model of pointers). The downside is no random access: because each element only knows about the next one, getting to index N means walking there from the start. There’s also a small per-element overhead for the pointer.
Array list requires a contiguous block of memory and stores elements in order, just like a plain array. Random access is fast — knowing the address of the first element lets you compute the address of element N with a single multiplication and addition. The downside is resizing: when the backing array fills up (default capacity is often 10), a new, larger array (usually 1.5×) has to be allocated, the old contents copied over, and the old array discarded. Inserting or removing at position N also requires shifting every element after N.
The strengths of one are precisely the weaknesses of the other. Which list to reach for depends on what the surrounding code does more often — random access or mid-list insertions.
Why Arrays.asList() exists
The original purpose of Arrays.asList() is to give a plain Java array the capabilities of a List — iterator, forEach, or the ability to be passed to any API that expects a List. That’s why the returned ArrayList uses the exact array you passed in as its backing storage: you can’t change the length (the array has a fixed size), but you can change the elements, and any change is reflected in the original array.
Why List.of() exists
The purpose of List.of() is to build an immutable data structure. This is valuable in multi-threaded and functional programming styles, where the safest way to share data is usually to make it read-only and require any modification to produce a fresh copy — similar in spirit to how Java’s ThreadLocal hands each thread its own isolated value.