What are the common pitfalls when converting between arrays and Lists in Java (Arrays.asList, toArray, primitive arrays)?
answer
- Arrays.asList = fixed-size view: add/remove throw, set writes through
- Arrays.asList(int[]) = List<int[]> size 1 (varargs trap); use stream().boxed()
- toArray() returns Object[]; use toArray(new T[0])
- Wrap in new ArrayList<>(...) for a mutable, independent list
- View vs copy: asList/subList alias; constructor/copyOf/toList copy
basics
~20 sArrays.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 sThe 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// 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
Knows that Arrays.asList(...).add(...) can throw and that you often wrap it in new ArrayList<>(...) to get a list you can modify.
Explains the fixed-size backed view, the primitive-array varargs trap, and the toArray(Object[] vs typed) distinction with the correct fixes.
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.
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)