skip to content

What are the pitfalls of Arrays.asList — its fixed-size backing behavior and how it handles a primitive array?

level: seniorimportance: must knowfreq 55%

answer

  1. Fixed-size view, NOT an ArrayList
  2. add/remove/clear -> UnsupportedOperationException; set is allowed
  3. Backed by array: write-through both ways
  4. asList(int[]) = List<int[]> of size 1 (box first!)
  5. Independent copy: new ArrayList<>(Arrays.asList(...))

basics

~20 s

Arrays.asList wraps an array as a List, but it's a fixed-size view backed by the array: you can't add or remove (those throw), and changes write through to the array. Also, asList(int[]) gives a List<int[]> with one element, not a List<Integer>; use a boxed Integer[] or a stream.

solid answer

~50 s

Arrays.asList returns a fixed-size List that is a live view over the array, not an independent ArrayList. Three things bite people. First, structural changes — add, remove, clear — throw UnsupportedOperationException because the size is fixed; only set (replacing an element) is allowed. Second, it's backed by the array: set on the list writes through to the array, and changes to the array show up in the list — they share storage. Third, with primitives, asList(int[]) infers T = int[] (since generics can't use primitives), so you get a List<int[]> of size 1 holding the whole array, not a List<Integer>; you must use Integer[] (boxed) or IntStream.of(a).boxed().collect(toList()). If you need a real, growable, independent list, wrap it: new ArrayList<>(Arrays.asList(...)). Also asList rejects null arrays and, being a view, isn't safe to keep if the underlying array escapes.

code

java · 15 lines
java
String[] arr = {"a", "b", "c"};
List<String> view = Arrays.asList(arr);

view.set(0, "z");                 // OK: writes through
System.out.println(arr[0]);       // "z"  -> backed view
// view.add("d");                 // throws UnsupportedOperationException (fixed size)

List<String> real = new ArrayList<>(Arrays.asList(arr)); // independent, growable
real.add("d");                    // fine; does not touch arr

// Primitive trap:
int[] nums = {1, 2, 3};
List<int[]> wrong = Arrays.asList(nums);   // size 1, element is the whole int[]
System.out.println(wrong.size());          // 1  (!)
List<Integer> right = Arrays.stream(nums).boxed().collect(Collectors.toList()); // size 3

go deeper

for a junior

Knows Arrays.asList turns an array into a List but that you can't add to the result.

for a middle

Explains fixed-size + write-through backing and wraps in new ArrayList<> when a growable list is needed.

for a senior

Articulates all three pitfalls (fixed size, backed view, primitive non-boxing), the null case, and chooses correctly between asList, ArrayList wrap, List.of, and streams.

for a principal

Reasons about API contracts and defensive copying (returning a view leaks internal state), aliasing/mutability hazards, and sets team conventions for safe array-to-collection conversion.

## What Arrays.asList is for `Arrays.asList(elements...)` bridges the array world and the `List` world: it lets you treat an array as a `java.util.List` so you can pass it to APIs that expect a collection, iterate it, or call read methods like `contains`. It is one of the most-used and most-misunderstood helpers. ## Pitfall 1 — it is FIXED-SIZE The returned list is **not** a `java.util.ArrayList`; it's a small private class (`Arrays$ArrayList`) whose **size is fixed** to the array's length. Any operation that would change the size throws **`UnsupportedOperationException`**: `add`, `remove`, `clear`, `addAll`, `removeIf`, etc. The only mutation allowed is **`set(i, value)`** (replace an element in place). People assume they got a normal resizable list and are surprised by the exception at runtime. ## Pitfall 2 — it is a BACKED VIEW (write-through) The list does not copy the array — it **wraps** it. They share the same backing storage: - `list.set(i, x)` writes `x` into `array[i]`. - Assigning `array[i] = y` is visible as `list.get(i) == y`. So mutations leak both ways. If the array later escapes or is mutated elsewhere, your 'list' changes under you. This is great for cheap interop but dangerous if you expected an independent snapshot. ## Pitfall 3 — primitives don't box automatically `Arrays.asList` is generic: `static <T> List<T> asList(T... a)`. Generics work only with **reference types**, not primitives. So: - `Arrays.asList(new int[]{1,2,3})` -> `T` is inferred as **`int[]`**, giving a **`List<int[]>` of size 1** whose single element is the whole array. `list.size()` is `1`, not `3`. A classic bug. - To get `List<Integer>`, pass a **boxed** array `Integer[]`, or `Arrays.asList(1, 2, 3)` (autoboxed varargs), or `IntStream.of(a).boxed().collect(Collectors.toList())`. Note `String[]` and other object arrays work fine: `Arrays.asList(new String[]{"a","b"})` is a `List<String>` of size 2, because they are reference types. ## Pitfall 4 — null array `Arrays.asList((Object[]) null)` throws **`NullPointerException`** — there is no array to wrap. ## How to get a REAL, independent, growable list Wrap it in an `ArrayList`: `List<String> real = new ArrayList<>(Arrays.asList(arr));`. This **copies** the elements into a normal `ArrayList`, so it is resizable and decoupled from the array. Alternatively, on modern Java, `List.of(arr)` gives an **immutable** copy (also fixed-size, but throws on any mutation including `set`, and rejects nulls), and `Stream.of(arr).collect(toList())` gives a mutable copy. ## Why it was designed this way `asList` is intentionally a thin, allocation-light **view** so converting an array to a `List` is cheap (no copy). The fixed size and write-through are the price of that cheapness. The contract is 'a List facade over your array', not 'a fresh collection'. ## Decision guide - Need a quick read-only/iterate facade, fine with array sharing -> `Arrays.asList(arr)`. - Need to add/remove or an independent copy -> `new ArrayList<>(Arrays.asList(arr))`. - Need an immutable copy -> `List.of(arr)`. - Have an `int[]`/primitive array -> box first (`IntStream...boxed()` or `Integer[]`). ## Mental model `Arrays.asList` puts a `List`-shaped window onto your array. The window can't be made bigger or smaller (fixed size), looking through it shows the array's current contents (backed view), and it only understands reference-type elements (primitives slip through as a single-element list).

  • Why does set() work on an Arrays.asList list but add() doesn't?
    set replaces an existing slot, which keeps the size constant and writes through to the backing array. add would change the size, but the list is fixed-size, so it throws UnsupportedOperationException.
  • How is List.of(arr) different from Arrays.asList(arr)?
    List.of returns a fully immutable copy: it doesn't back the array, rejects nulls, and throws on every mutation including set. Arrays.asList is a mutable-by-set, write-through, fixed-size view over the original array.

saying these in an interview costs you the question

  • Calling add/remove on an Arrays.asList result and expecting it to grow
  • Treating the result as an independent copy of the array
  • Passing an int[] and expecting List<Integer> of the elements
  • Holding onto the view while the underlying array gets mutated elsewhere

context