skip to content

How does Arrays.asList illustrate the Adapter pattern, and what surprising behaviors come from its view semantics?

level: middleimportance: should knowfreq 50%

answer

  1. View backed by the array, not a copy
  2. set works (writes through); add/remove throw UnsupportedOperationException
  3. Fixed-size because arrays can't grow
  4. int[] -> single-element List<int[]> (varargs)
  5. Copy with new ArrayList<>(...) for a real mutable list

basics

~20 s

Arrays.asList wraps an array in the List interface without copying it. The list is a fixed-size view backed by the array: changing the list changes the array and vice versa, set works, but add and remove throw UnsupportedOperationException.

solid answer

~40 s

Arrays.asList adapts a raw array (the adaptee) to the List interface (the target) by returning a thin fixed-size view backed by the same array — no copy is made. Because it shares storage, set(i, v) writes through to the array and array mutations are visible through the list. But the list is fixed-size: add and remove throw UnsupportedOperationException, since an array can't grow. Two classic gotchas: passing a primitive array like int[] makes a single-element List<int[]> (varargs sees one Object), so use boxed types or streams; and because the returned list is a live view, mutating the backing array under it can surprise callers. To get a real mutable, growable list, copy it: new ArrayList<>(Arrays.asList(...)). The adapter intent is reuse: array data usable wherever a List is expected, without converting it.

code

java · 6 lines
java
String[] arr = { "a", "b", "c" };
List<String> view = Arrays.asList(arr); // adapter: array -> List view
view.set(0, "z");        // OK, writes through; arr[0] is now "z"
view.add("d");           // UnsupportedOperationException (fixed size)

List<String> mutable = new ArrayList<>(Arrays.asList(arr)); // real growable copy

go deeper

for a junior

Knows Arrays.asList turns an array into a List and that you often wrap it in new ArrayList<> to add elements.

for a middle

Explains the fixed-size live-view semantics, that set writes through while add/remove throw, and the primitive-array varargs gotcha.

for a senior

Frames it as an Adapter returning an O(1) view, weighs view vs copy vs List.of, and anticipates shared-state and immutability hazards in API design.

for a principal

Reasons about leaky-abstraction risk of returning live views across module boundaries and when to expose copies/immutable collections to protect invariants.

## The adapter at work A Java **array** (`T[]`) and the **`List<T>`** interface are different types with different APIs. `Arrays.asList(T... a)` bridges them: it returns an object that **implements `List`** but is **backed by the original array**. That is textbook Adapter — the array is the *adaptee*, `List` is the *target*, and the returned `Arrays$ArrayList` is the *adapter*. Crucially it returns a **view**, not a copy: both refer to the same underlying storage. ```java String[] arr = { "a", "b", "c" }; List<String> view = Arrays.asList(arr); view.set(0, "z"); // writes through System.out.println(arr[0]); // "z" - the array changed too ``` ## Surprising behavior 1: fixed size An array's length is immutable, so the view can't support structural changes. `set` (replace in place) works, but: ```java view.add("d"); // UnsupportedOperationException view.remove(0); // UnsupportedOperationException view.clear(); // UnsupportedOperationException ``` The view implements `List` but overrides only the parts an array can honor. To get a fully mutable list, **copy** it: ```java List<String> mutable = new ArrayList<>(Arrays.asList(arr)); ``` ## Surprising behavior 2: live, shared state Because it shares storage, writes go both ways. Mutating `arr` later is visible through `view`, and vice versa. This is intentional (it's a view) but can bite if a caller assumes a snapshot. ## Surprising behavior 3: primitive arrays `Arrays.asList` is **varargs of `T`** (`T...`), and primitives aren't objects. Pass an `int[]` and the compiler treats the whole array as a single `Object` argument: ```java int[] nums = { 1, 2, 3 }; List<int[]> wrong = Arrays.asList(nums); // size 1! one int[] element wrong.size(); // 1 ``` Use a boxed array (`Integer[]`) or a stream instead: ```java List<Integer> right = Arrays.stream(nums).boxed().toList(); ``` ## Why a view (the design rationale) Returning a view is O(1) and allocation-light, and lets array-only code interoperate with `List`-based APIs cheaply. The cost is the leaky abstraction: callers must know it's fixed-size and live. `List.of(...)` (Java 9+) is a different beast — a truly **immutable** copy — so reach for it when you want an unmodifiable list and `new ArrayList<>(...)` when you want a growable one. ## Summary of the contract | Operation | Behavior | |---|---| | `get` / `set` | Works, writes through to the array | | `size`, iteration | Works | | `add` / `remove` / `clear` | `UnsupportedOperationException` | | backing array mutated | Visible through the list (live view) | | primitive array argument | Becomes a single-element `List<int[]>` |

  • Why does Arrays.asList(intArray) compile but produce a one-element list?
    asList takes T... (varargs of objects). A primitive int[] isn't a T[], so the whole array binds to a single T = int[] argument, yielding a List<int[]> of size 1. Use Integer[] or Arrays.stream(intArray).boxed().
  • How do you turn the result into a growable list?
    Wrap it: new ArrayList<>(Arrays.asList(...)), which copies the elements into an independent, resizable ArrayList no longer tied to the original array.

saying these in an interview costs you the question

  • Assuming Arrays.asList returns a normal growable ArrayList.
  • Thinking it copies the array (it's a live view).
  • Calling add/remove on it without expecting UnsupportedOperationException.
  • Passing a primitive array and assuming each element becomes a list element.

context