Arrays.asList 有哪些坑?返回的 List 能 add 吗?
简化版
Arrays.asList 有三个经典坑:① 返回的是固定大小的 List,调 add/remove 抛 UnsupportedOperationException(它是 Arrays 内部类,不是 ArrayList);② 它是原数组的视图,改 List 会改原数组,反之亦然;③ 传基本类型数组(如 int[])时,会把整个数组当成一个元素,得到 List<int[]> 而不是 List<Integer>。要一个可增删的真 List,用 new ArrayList<>(Arrays.asList(arr))。
详细版
坑 1:固定大小,不能增删
List<String> list = Arrays.asList("a", "b", "c");
list.add("d"); // ❌ UnsupportedOperationException
list.remove(0); // ❌ UnsupportedOperationException
list.set(0, "x"); // ✅ 允许!set 只改现有位置,不改变大小
Arrays.asList 返回的是 java.util.Arrays$ArrayList(一个私有内部类),不是 java.util.ArrayList。它没有实现 add/remove(继承 AbstractList 的默认实现直接抛异常),因为它背后是个定长数组,大小不能变。
坑 2:是数组的视图,双向联动
String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);
list.set(0, "x"); // 改 list
System.out.println(arr[0]); // "x" !原数组也变了
arr[1] = "y"; // 改数组
System.out.println(list.get(1)); // "y" !list 也变了
它没有复制数组,只是包装了原数组的引用,所以两者共享同一份数据。
坑 3:基本类型数组变成单元素 List
int[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
System.out.println(list.size()); // 1 !不是 3
// 泛型推断成 List<int[]>,整个数组是唯一的一个元素
Integer[] arr2 = {1, 2, 3}; // 用包装类型才对
List<Integer> list2 = Arrays.asList(arr2);
System.out.println(list2.size()); // 3 ✓
⚠️ 正确得到「可增删的独立 List」:
List<String> ok = new ArrayList<>(Arrays.asList(arr));——包一层 ArrayList,既能增删,也和原数组脱钩。
完整版教学
一、为什么 add 会抛异常:它根本不是 ArrayList
最反直觉的一点:Arrays.asList 的返回类型写着 List,但它不是 java.util.ArrayList,而是 Arrays 类里的一个私有静态内部类 Arrays$ArrayList。这个类:
java.util.Arrays$ArrayList ≠ java.util.ArrayList
├── 继承 AbstractList
├── 内部只持有一个 E[] 数组(就是你传进来的)
├── 实现了 set/get/size(能读能改现有元素)
└── 没有重写 add/remove → 用 AbstractList 的默认实现 → 直接 throw UnsupportedOperationException
AbstractList 里 add/remove 的默认实现就是抛 UnsupportedOperationException。这个内部类故意不实现它们,因为它背后是定长数组——数组长度不可变,自然不能增删。所以 set(i, v)(替换现有槽位,不改大小)能用,add/remove(改变大小)不能用。理解「它是数组的薄包装」,三个坑就都通了。
二、视图机制:共享数组、双向联动
Arrays.asList(arr) 不复制数组,只是让新 List 内部字段指向同一个 arr。于是它是原数组的一个「视图(view)」,读写都作用在同一份底层数据上:
String[] arr ──┐
├──▶ 同一块内存 ["a","b","c"]
List list ────┘
list.set(0,"x") → 底层[0] 变 "x" → arr[0] 也是 "x"
arr[1] = "y" → 底层[1] 变 "y" → list.get(1) 也是 "y"
这既是特性也是坑:好处是零拷贝、轻量;坏处是你以为拿到了独立副本,改一个却动了另一个,容易埋下隐蔽 bug。要脱钩必须显式复制:new ArrayList<>(Arrays.asList(arr)) 会把元素逐个拷进新 ArrayList,从此和原数组无关。
三、基本类型数组的陷阱:泛型不认 int
这是最隐蔽的坑,根源在泛型不支持基本类型。Arrays.asList 的签名是 static <T> List<T> asList(T... a)——它接收的是「对象数组」。而 int[] 里装的是基本类型 int,不是对象:
asList(int[] arr):
T 只能是引用类型 → T 被推断为 int[] 本身
于是 int[] 被当作「一个 T 元素」→ 返回 List<int[]>,size=1
asList(Integer[] arr):
T 推断为 Integer,可变参数展开 → List<Integer>,size=3 ✓
编译器把整个 int[] 当成可变参数里的单个对象,所以 size() 是 1。用 long[]、double[]、char[] 等基本类型数组都会踩这个坑。解决办法:用包装类型数组 Integer[],或 Java 8+ 用 Arrays.stream(arr).boxed().collect(toList())。
四、三个坑的统一根因与对照
三个坑看似无关,其实都源于「它是定长原数组的薄视图 + 泛型只认对象」这一个事实:
| 坑 | 现象 | 根因 |
|---|---|---|
| 不能 add/remove | 抛 UnsupportedOperationException | 背后是定长数组,Arrays$ArrayList 没实现增删 |
| 改一个动另一个 | List 和数组双向联动 | 没复制,共享同一份数组引用(视图) |
| int[] 变 size=1 | List<int[]> 而非 List<Integer> | 泛型 T 只能是对象,int[] 被当单个元素 |
记住这条主线,就不用死记三个孤立结论。
五、正确用法与替代方案
// ① 只读、不改、数据量小 → Arrays.asList 就够(还省内存)
for (String s : Arrays.asList("a", "b")) { ... } // 遍历没问题
// ② 需要可增删的独立 List → 包一层 ArrayList
List<String> mutable = new ArrayList<>(Arrays.asList(arr));
mutable.add("d"); // ✅
// ③ 基本类型数组转 List<包装类型> → Stream
List<Integer> nums = Arrays.stream(new int[]{1,2,3})
.boxed().collect(Collectors.toList());
// ④ Java 9+ 真·不可变 List(连 set 都不行)
List<String> immutable = List.of("a", "b", "c");
注意 Arrays.asList 和 List.of 都「不可增删」,但性质不同:前者能 set(改现有槽位)且是数组视图;后者完全不可变(set 也抛异常)、且不允许 null 元素。面试常拿这俩对比。
六、List.of / Collections.emptyList 的相关辨析
顺带厘清几个「看着像 ArrayList 其实不是」的返回值,它们都是不可变或定长的,误当普通 List 去 add 都会抛异常:
| 来源 | 可 add/remove | 可 set | 允许 null | 是否视图 |
|---|---|---|---|---|
new ArrayList<>() | ✅ | ✅ | ✅ | 否 |
Arrays.asList(arr) | ❌ | ✅ | ✅ | 是(数组视图) |
List.of(...)(Java 9+) | ❌ | ❌ | ❌ | 否(独立不可变) |
Collections.emptyList() | ❌ | ❌ | — | 否(空单例) |
Collections.unmodifiableList(l) | ❌ | ❌ | 看原 list | 是(只读视图) |
Stream.collect(toList()) | ✅(通常 ArrayList) | ✅ | ✅ | 否 |
一句话经验:任何「工厂方法/包装方法」返回的 List,别默认它能增删;要可变就自己 new ArrayList<>(...) 包一层。
记忆钩子:「asList 是数组视图:能 set 不能 add、改一个动俩、int[] 变一个」;要真 List 就
new ArrayList<>(asList(...))。
七、常见误区与追问
- 误区:Arrays.asList 返回的是 ArrayList。 是
Arrays$ArrayList内部类,不是 java.util.ArrayList,没实现 add/remove。 - 误区:它返回的 List 完全不能改。 能
set(替换现有元素),只是不能 add/remove(改变大小);这点和List.of的完全不可变不同。 - 误区:
Arrays.asList(new int[]{1,2,3})得到含 3 个元素的 List。 得到 size=1 的List<int[]>,整个 int 数组是唯一元素;要用 Integer[] 或 Stream.boxed。 - 误区:改了返回的 List 不影响原数组。 它是视图,set 会同步改原数组,反之亦然;要脱钩得 new ArrayList 复制。
- 追问:怎样得到可增删且独立于数组的 List?
new ArrayList<>(Arrays.asList(arr)),既能增删又和原数组解耦。 - 追问:Arrays.asList 和 List.of 有什么区别? asList 能 set、是数组视图、允许 null;List.of 完全不可变、独立、不允许 null 元素,Java 9+ 才有。
- 追问:为什么设计成视图而不复制? 为轻量和零拷贝——常见用途是把数组临时当 List 遍历/传参,不复制更省内存;副作用是联动,需使用者知情。
八、加强记忆
Arrays.asList 的三个坑其实是一个真相的三个侧面:它只是「定长原数组」的一层薄视图,加上泛型只认对象。因为背后是定长数组,所以能 set 不能 add/remove(返回的是 Arrays$ArrayList 内部类,不是真 ArrayList,增删直接抛 UnsupportedOperationException);因为不复制、共享数组引用,所以改 List 动数组、改数组动 List;因为泛型 T 只能是引用类型,所以传 int[] 会把整个数组当一个元素得到 List<int[]>(size=1),要用 Integer[] 或 stream().boxed()。想要「可增删又独立」的真 List,永远记得 new ArrayList<>(Arrays.asList(arr)) 包一层。再顺手记住「工厂方法返回的 List(List.of / emptyList / unmodifiableList)默认都不可增删」,这类陷阱题就再也不会栽了。