skip to content

What are the common pitfalls when converting between arrays and Lists in Java (Arrays.asList, toArray, primitive arrays)?

level: middleimportance: should knowfreq 50%

answer

  1. Arrays.asList = fixed-size view: add/remove throw, set writes through
  2. Arrays.asList(int[]) = List<int[]> size 1 (varargs trap); use stream().boxed()
  3. toArray() returns Object[]; use toArray(new T[0])
  4. Wrap in new ArrayList<>(...) for a mutable, independent list
  5. View vs copy: asList/subList alias; constructor/copyOf/toList copy

basics

~20 s

Arrays.asList gives a fixed-size list backed by the array, so add/remove throws and changes write through to the array. Passing a primitive array like int[] to asList makes a one-element List of the array, not a list of ints. To get a real resizable list, wrap it: new ArrayList<>(Arrays.asList(...)).

solid answer

~40 s

The classic traps: (1) Arrays.asList returns a fixed-size, array-backed List view — add() or remove() throws UnsupportedOperationException, and set() writes through to the underlying array. Wrap it in new ArrayList<>(...) to get a mutable, independent list. (2) Arrays.asList(intArray) with a primitive int[] doesn't give List<Integer>; because int[] isn't Object[], it's treated as a single varargs argument, yielding a List<int[]> of size 1 — use Arrays.stream(arr).boxed().toList() instead. (3) toArray() with no argument returns Object[], not your element type; use the typed overload list.toArray(new String[0]) or toArray(String[]::new). (4) Converting back and forth copies data and can lose the live-view vs copy distinction. Knowing whether you have a view (asList, Arrays.asList) or an independent copy (new ArrayList<>) is the key to avoiding surprising aliasing bugs.

code

java · 13 lines
java
// 1. Fixed-size, backed view
List<String> view = Arrays.asList("a", "b", "c");
// view.add("d");           // UnsupportedOperationException
view.set(0, "X");           // writes through to the backing array
List<String> mutable = new ArrayList<>(view); // independent, growable copy

// 2. Primitive-array varargs trap
int[] nums = {1, 2, 3};
List<?> oops = Arrays.asList(nums);           // List<int[]> of size 1
List<Integer> ok = Arrays.stream(nums).boxed().toList(); // [1, 2, 3]

// 3. Typed toArray
String[] arr = mutable.toArray(new String[0]);

go deeper

for a junior

Knows that Arrays.asList(...).add(...) can throw and that you often wrap it in new ArrayList<>(...) to get a list you can modify.

for a middle

Explains the fixed-size backed view, the primitive-array varargs trap, and the toArray(Object[] vs typed) distinction with the correct fixes.

for a senior

Frames everything as view-vs-copy aliasing, knows the toArray(new T[0]) idiom and the List.of immutability differences, and writes conversions that avoid hidden mutation coupling.

for a principal

Sets API conventions (return immutable copies vs documented views), reasons about defensive copying at module boundaries, and weighs the performance/clarity trade-offs of conversion patterns across a large codebase.

## Why conversions are tricky Arrays and Lists are different worlds — one is a language built-in, the other a library type — so Java provides bridge methods. Each bridge has a sharp edge. Let's walk them. ## Pitfall 1: Arrays.asList returns a fixed-size, backed view `Arrays.asList(a, b, c)` looks like it makes a normal list, but it returns a **special fixed-size List** that is a **view over the original array**. Two consequences: ```java List<String> list = Arrays.asList("a", "b", "c"); list.add("d"); // throws UnsupportedOperationException — size is fixed list.set(0, "X"); // allowed — and it writes THROUGH to the backing array ``` You **cannot** add or remove (the size is locked to the array's length), but you **can** `set()`, and that mutation is reflected in the original array (and vice-versa) because the list is just a wrapper around it. If you want a genuine, resizable, independent list, **copy it**: ```java List<String> mutable = new ArrayList<>(Arrays.asList("a", "b", "c")); mutable.add("d"); // fine — independent copy ``` ## Pitfall 2: primitive arrays and varargs `Arrays.asList` is **varargs**: `asList(T... a)`. Varargs only works with **objects** (T is a reference type). So if you pass a **primitive array**: ```java int[] nums = {1, 2, 3}; List<?> wrong = Arrays.asList(nums); // List<int[]> of SIZE 1, not List<Integer> of size 3! System.out.println(wrong.size()); // prints 1 ``` Because `int[]` is itself a single Object (it is not an `Integer[]`), the compiler treats the whole array as **one** varargs argument, giving you a one-element `List<int[]>`. People are baffled when `.size()` is 1. The fix is to **box via a stream**: ```java List<Integer> right = Arrays.stream(nums).boxed().toList(); // [1, 2, 3] ``` With an **object** array (`Integer[]`, `String[]`) `Arrays.asList` works as expected, spreading the elements. ## Pitfall 3: toArray() returns Object[] The no-arg `list.toArray()` returns **`Object[]`**, not your element type — you can't cast it to `String[]` (that throws `ClassCastException`). Use the **typed overload**: ```java List<String> list = List.of("a", "b"); String[] bad = (String[]) list.toArray(); // ClassCastException String[] good = list.toArray(new String[0]); // correct & idiomatic String[] also = list.toArray(String[]::new); // Java 11+ generator form ``` Passing `new String[0]` (an empty array) is the recommended idiom — modern JITs make it as fast as a pre-sized array, and it lets the runtime create an array of the right reified type. ## Pitfall 4: view vs copy aliasing The deeper theme: always know whether a conversion gives you a **live view** (shares storage, mutations alias) or an **independent copy**. `Arrays.asList` and `List.subList` are views; `new ArrayList<>(other)`, `Arrays.copyOf`, `clone()`, and `stream().toList()` are copies. Mixing them up causes spooky-action bugs where mutating one container silently changes another. ## Bonus: List.of / Arrays.asList immutability differences `List.of(...)` (Java 9+) returns a **fully immutable** list — both add/remove **and** set throw. `Arrays.asList` is fixed-size but `set`-able. `List.of` also rejects `null` elements. Pick `List.of` for true constants, `new ArrayList<>(...)` for something you'll mutate. ## Cheat-sheet - Need a mutable list from values → `new ArrayList<>(Arrays.asList(...))` or `new ArrayList<>(List.of(...))`. - Need a true immutable list → `List.of(...)`. - int[] → List<Integer> → `Arrays.stream(a).boxed().toList()`. - List → typed array → `list.toArray(new T[0])`. - Remember: asList/subList are **views**; constructors/copyOf/toList are **copies**.

  • Why does Arrays.asList(new int[]{1,2,3}).size() return 1?
    asList is varargs over a reference type T. An int[] is a single Object (not Integer[]), so the whole array is taken as one varargs element, producing a List<int[]> with one entry. Use Arrays.stream(arr).boxed().toList() to get List<Integer> of the three values.
  • What's the difference between List.of(...) and new ArrayList<>(Arrays.asList(...))?
    List.of returns a fully immutable list (add, remove, and set all throw; nulls rejected). new ArrayList<>(Arrays.asList(...)) returns a fully mutable, independent copy you can add to, remove from, and modify freely.

saying these in an interview costs you the question

  • Thinking Arrays.asList returns a normal growable ArrayList
  • Expecting Arrays.asList(intArray) to give a List of the int values
  • Casting toArray() result directly to String[]
  • Not realizing set() on an asList view mutates the original array
  • Confusing List.of (fully immutable) with Arrays.asList (fixed-size but set-able)

context