我用 Arrays.asList 把数组转成 List 然后往里 add 元素,运行时抛了 UnsupportedOperationException,代码编译明明全过,我对着这个"只读 List"排查了大半天的复盘
那是我写的一段很普通的 Java 代码:把一个数组用 Arrays.asList() 转成 List,然后根据业务逻辑往这个 List 里 add 几个元素。代码写得行云流水,编译器一声没吭、全程绿灯。可一运行,就抛了一个我当时完全没想到的异常:java.lang.UnsupportedOperationException。我盯着代码满脸问号:它的类型明明是 List,List 不就该能 add 吗?编译都过了,凭什么运行时说"不支持这个操作"?我甚至怀疑是不是 JDK 版本的问题。排查了大半天,我才真正理解了 Arrays.asList() 返回的那个"List",根本不是我以为的那个普通 ArrayList。这篇就把这场"List 不能 add"的事故,从头复盘一遍。
故障现场:类型是 List,却不能 add
先看现场。一个看着普普通通的 List,却在 add 时翻了脸:
// 我的代码: 数组转 List, 然后 add
String[] arr = {"a", "b", "c"};
List list = Arrays.asList(arr); // 看着是个 List
list.add("d"); // ✗✗ 运行时 panic: UnsupportedOperationException!
list.remove("a"); // ✗ 同样抛 UnsupportedOperationException
list.clear(); // ✗ 同样抛
// 但是这些操作却可以:
list.get(0); // ✓ 读取 OK
list.set(0, "x"); // ✓ 修改已有元素 OK (注意: 这个能改!)
list.size(); // ✓ OK
// 异常:
// Exception in thread "main" java.lang.UnsupportedOperationException
// at java.base/java.util.AbstractList.add(AbstractList.java:...)
// 为什么? Arrays.asList 返回的不是 java.util.ArrayList!
// - Arrays.asList() 返回的, 是 Arrays 类内部的一个【私有静态内部类】
// java.util.Arrays$ArrayList(注意: 是 Arrays 的内部类, 不是 java.util.ArrayList!)
// - 这个内部类, 是对【原数组的一层包装(视图 view)】:
// 它的底层【直接就是你传进去的那个数组】, 长度固定(数组长度不可变)。
// - 所以它:
// * 支持 get/set(读和"改已有元素", 因为不改变长度, 直接操作底层数组)。
// * 不支持 add/remove/clear(这些要改变长度, 而数组长度是固定的!)
// → 这些方法它没重写, 用的是父类 AbstractList 的默认实现 = 直接抛
// UnsupportedOperationException。
// - 更坑的: 因为它是数组的"视图", 改 list 的元素会【影响原数组】, 反之亦然!
// 现象拼图:
// - 我以为 Arrays.asList() 返回的是"普通的、可增删的 ArrayList"。
// - 实际它返回的是"基于原数组的、固定大小的、只能读和改不能增删的视图"。
// - 它的静态类型是 List(所以编译过), 但运行时的实际实现不支持 add。
// - ★ 根因: 我把"返回类型是 List"等同于"它是一个功能完整的可变 ArrayList",
// 而忽略了 List 只是接口, 不同实现的能力可以天差地别。
看清真相后,我才明白这个"List"的真面目。问题的根源,是 Arrays.asList() 返回的根本不是 java.util.ArrayList,而是 Arrays 类内部的一个私有静态内部类 java.util.Arrays$ArrayList(同名但完全不同!)。这个内部类是对原数组的一层包装(视图):它的底层直接就是你传进去的那个数组,长度固定(数组长度不可变)。所以它:支持 get/set(读和改已有元素,不改变长度),但不支持 add/remove/clear(这些要改变长度,而数组长度固定)——这些方法它没重写,用的是父类 AbstractList 的默认实现,直接抛 UnsupportedOperationException。更坑的是:因为它是数组的"视图",改 list 的元素会影响原数组,反之亦然。我犯的根本错误是:把"返回类型是 List"等同于"它是一个功能完整的可变 ArrayList",而忽略了 List 只是接口,不同实现的能力可以天差地别。
第一件事:搞懂 List 是接口、实现各有差异
要解决它,得先理解 Java 里"接口"和"实现"的关系,以及 Arrays.asList 这个"视图"的本质。
List 是接口, 不同实现能力天差地别
# 一、List 只是一个"接口", 定义了"能做什么", 不规定"怎么做、是否支持"
# - List 接口里, add/remove 等方法被定义为"可选操作(optional operation)"。
# - 接口文档明确写: 实现【可以】不支持某些操作, 不支持就抛
# UnsupportedOperationException。
# - 所以: "类型是 List" 只保证"有 add 这个方法可调", 【不保证调用一定成功】!
# 二、不同 List 实现的"可变性"天差地别:
# - new ArrayList<>() : 完全可变(增删改查都行)。
# - Arrays.asList(arr) : 固定大小(可 set, 不可 add/remove)。本文!
# - List.of(...) : 完全不可变(set/add/remove 全不行, JDK9+)。
# - Collections.unmodifiableList(): 只读视图(任何修改都抛异常)。
# - Collections.emptyList() : 不可变的空列表。
# → 它们都是 List, 但"能不能改、能改到什么程度", 完全不同!
# 三、为什么 Arrays.asList 这么设计?
# - 它的定位是"把数组方便地'看成'一个 List"(做个适配/桥接), 而不是
# "把数组拷贝成一个全新的可变 List"。
# - 它是数组的"视图(view)": 共享底层数组、长度跟数组一样固定。
# - 这样设计很轻量(不拷贝数据), 但代价就是"不能增删"。
# - 它的典型用途: 快速创建一个用于"遍历/读取"的 List(如 for-each、传参),
# 而不是用来"增删"。
# 四、关键认知: 面向接口编程时, 要注意"可选操作"
# - 拿到一个 List, 别假设它一定可变。
# - 尤其是"别人返回给你的 List"、"工具方法创建的 List", 可能是只读/固定的。
# - 要可变, 就自己确保它是可变实现(如 new ArrayList<>(原list))。
# 核心: List是接口, add/remove是"可选操作", 实现可不支持(抛UnsupportedOperationException);
# Arrays.asList是数组的固定大小视图(可set不可add); List.of/unmodifiableList完全只读; 要可变自己包成ArrayList。
想透"接口 vs 实现"这层关系,这个异常就不再神秘了。一、List 只是接口,定义"能做什么"不规定"是否支持"——List 接口里 add/remove 被定义为"可选操作",文档明确写实现可以不支持、不支持就抛 UnsupportedOperationException;所以"类型是 List"只保证"有 add 方法可调",不保证"调用一定成功"。二、不同 List 实现的可变性天差地别:new ArrayList(完全可变)、Arrays.asList(固定大小,可 set 不可 add,本文)、List.of(完全不可变,JDK9+)、Collections.unmodifiableList(只读视图)、Collections.emptyList(不可变空表)——都是 List,但能不能改完全不同。三、为什么 Arrays.asList 这么设计?——它的定位是"把数组方便地看成一个 List"(适配/桥接),是数组的视图(共享底层数组、长度固定),很轻量(不拷贝)但不能增删,典型用途是快速创建一个用于遍历/传参的 List。四、关键认知:面向接口编程要注意"可选操作"——拿到一个 List 别假设它一定可变,尤其别人返回的/工具方法创建的;要可变就自己包成 new ArrayList<>(原list)。
第二件事:正解——要可变就包一层 new ArrayList
搞懂了原理,正解就清晰了:要可变的 List,就用 new ArrayList 包一层(拷贝);明确不可变就用 List.of;别假设工具方法返回的 List 可变。
String[] arr = {"a", "b", "c"};
// ====== 正解一(推荐): 需要可变, 就用 new ArrayList 包一层 ======
List list = new ArrayList<>(Arrays.asList(arr));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// → new ArrayList<>(collection) 会把元素【拷贝】到一个全新的、可变的
// ArrayList 里。它和原数组无关了, 可以随意 add/remove。
list.add("d"); // ✓ OK!
list.remove("a"); // ✓ OK!
// → 这是"数组转可变List"最常用、最稳的写法。
// ====== 正解二: 只是要遍历/读取, Arrays.asList 就够了(别 add 它)======
for (String s : Arrays.asList(arr)) { ... } // ✓ 遍历没问题
// 当参数传给只读的方法也 OK:
process(Arrays.asList(arr)); // ✓ 只要 process 不往里 add
// ====== 正解三: 明确要"不可变 List", 用 List.of(JDK9+)======
List immutable = List.of("a", "b", "c"); // 完全不可变
// immutable.add("d"); // ✗ UnsupportedOperationException(这是它的设计意图)
// → 如果你就是想要个"谁都改不了"的常量列表, 用 List.of, 语义清晰。
// ====== 正解四: 单元素/可变的便捷创建 ======
// JDK 没有内置的可变 List 字面量, 常用:
List a = new ArrayList<>(List.of("x", "y")); // 可变 + 初始值
List b = new ArrayList<>(); // 空的可变
Collections.addAll(b, "x", "y"); // 批量加
// ====== 正解五: 注意 Arrays.asList 的两个附加坑 ======
// 坑a: 传基本类型数组时, 不会拆成元素!
int[] nums = {1, 2, 3};
List wrong = Arrays.asList(nums); // ✗ 这是 List! 只有1个元素(整个数组)!
List right = Arrays.asList(1, 2, 3); // ✓ 用包装类型 Integer[]
// → 基本类型数组要转 List, 用 IntStream.of(nums).boxed().collect(...)。
// 坑b: 它是数组的视图, 改 list 影响原数组:
List view = Arrays.asList(arr);
view.set(0, "X"); // 改了 list
System.out.println(arr[0]); // "X" ← 原数组也变了! (共享底层)
// 核心: 要可变就 new ArrayList<>(Arrays.asList(...)) 拷贝一份; 只读遍历用Arrays.asList即可;
// 要不可变用List.of; 注意基本类型数组不拆元素、且asList是改了影响原数组的视图。
修复的核心,是"想清楚你要的 List 是可变还是不可变,用对应的创建方式"。正解一(推荐):需要可变就用 new ArrayList<>(Arrays.asList(arr)) 包一层——它会把元素拷贝到一个全新的、可变的 ArrayList 里,和原数组无关,可随意 add/remove,这是"数组转可变 List"最常用最稳的写法。正解二:只是遍历/读取,Arrays.asList 就够了(别 add 它,传给只读方法也 OK)。正解三:明确要不可变就用 List.of(JDK9+)——想要"谁都改不了"的常量列表时语义清晰。正解四:可变 List 的便捷创建(new ArrayList<>(List.of(...)))。还有两个附加坑:正解五a:传基本类型数组(int[])时不会拆成元素(Arrays.asList(nums) 是 List<int[]> 只有 1 个元素!要用包装类型或 stream);正解五b:它是数组的视图,改 list 影响原数组(共享底层)。归根结底:要可变就 new ArrayList<>(Arrays.asList(...)) 拷贝;只读遍历用 Arrays.asList;要不可变用 List.of;注意基本类型数组不拆元素、且 asList 是视图。
第三件事:Java 各种创建 List 方式的可变性
排查后我把 Java 里各种创建 List 的方式、以及它们的可变性,系统梳理了一遍。
Java 创建 List 的各种方式 与 可变性
# 完全可变(可 add/remove/set):
# new ArrayList<>() : 标准可变列表
# new ArrayList<>(otherCollection) : 拷贝其他集合的可变列表
# new LinkedList<>() : 链表实现, 可变
# 固定大小(可 set, 不可 add/remove):
# Arrays.asList(a, b, c) : 数组视图, 本文! 长度固定
# → 想可变: new ArrayList<>(Arrays.asList(...))
# 完全不可变(set/add/remove 全不行):
# List.of(a, b, c) : JDK9+ 不可变列表
# List.copyOf(other) : JDK10+ 不可变拷贝
# Collections.unmodifiableList(list) : 只读视图(底层list变它也变)
# Collections.emptyList() : 不可变空列表
# Collections.singletonList(x) : 不可变单元素列表
# stream().collect(toUnmodifiableList()): JDK10+ 不可变
# 可变(stream 收集):
# stream().collect(Collectors.toList()): 通常是可变的ArrayList
# (但 JDK 不保证具体实现/可变性! 要可变建议 toCollection(ArrayList::new))
# 一个判断口诀:
# - new XxxList(): 可变。
# - Arrays.asList: 固定大小(可改不可增删)。
# - List.of / Collections.unmodifiable* / emptyList / singletonList: 不可变。
# - 拿不准、且要修改它: 就 new ArrayList<>(它) 拷贝一份, 万无一失。
# 核心: new ArrayList可变、Arrays.asList固定大小(可set不可增删)、List.of/unmodifiable*完全不可变;
# 拿不准又要改就 new ArrayList<>(它) 拷贝; 别假设任意来源的List都可变。
排查让我把 Java 创建 List 的方式彻底厘清了。完全可变(可 add/remove/set):new ArrayList、new ArrayList(otherCollection)、new LinkedList。固定大小(可 set 不可增删):Arrays.asList(本文,数组视图)。完全不可变:List.of、List.copyOf、Collections.unmodifiableList、emptyList、singletonList、toUnmodifiableList。判断口诀:new XxxList() 可变、Arrays.asList 固定大小、List.of/unmodifiable*/emptyList/singletonList 不可变;拿不准且要修改它,就 new ArrayList<>(它) 拷贝一份万无一失。它给我的启发是:"创建一个 List"这件看似简单的事,背后有这么多种方式、且可变性各不相同;而它们都返回 List 类型、编译期完全看不出区别——这正是这个坑的隐蔽之处。下面这张图,是这次 List 不能 add 的成因与解法:
第四件事:各种 List 创建方式可变性速查
这次踩坑后,我把常见的 List 创建方式和它们的可变性整理成一张表,贴在工位上对照。
| 创建方式 | add/remove | set | 说明 |
|---|---|---|---|
| new ArrayList<>() | ✓ | ✓ | 标准可变,默认选它 |
| new ArrayList<>(coll) | ✓ | ✓ | 拷贝成可变,推荐 |
| Arrays.asList(...) | ✗ 抛异常 | ✓ | 数组视图,固定大小(本文) |
| List.of(...) | ✗ 抛异常 | ✗ 抛异常 | 完全不可变,JDK9+ |
| Collections.unmodifiableList | ✗ 抛异常 | ✗ 抛异常 | 只读视图 |
| Collections.emptyList() | ✗ 抛异常 | ✗ | 不可变空列表 |
| stream().collect(toList()) | 通常✓ | 通常✓ | 不保证,要可变用toCollection |
这张表,把"各种 List 到底能不能改"一网打尽了。它清楚地显示:同样是 List 类型,它们的可变性从"完全可变"到"固定大小"到"完全不可变"都有,而且编译期完全看不出区别——只有运行时调用了不支持的操作,才会以异常的形式暴露。它给我的启发是:Java 的 List 接口,通过"可选操作 + 抛 UnsupportedOperationException"的方式,在"统一的接口"下,容纳了"能力各异的实现"。这种设计有它的好处(所有 List 能用统一的方式遍历、传递),但也有代价:"类型相同"不再意味着"能力相同";一个 List 变量,它实际支持哪些操作,取决于它运行时的具体实现,而这在编译期是隐藏的。这让我领悟到面向接口编程的一个微妙之处:接口给了我们"面向抽象、不依赖具体实现"的灵活,但也要求我们清醒地意识到:"接口承诺的方法"和"具体实现真正支持的操作",可能存在差距(可选操作);尤其当一个对象的"来源"不明确时(别人返回的、工具创建的),不能想当然地假设它支持接口的全部能力。面向接口编程,既要善用它的抽象,也要警惕它"隐藏了实现差异"的那一面。
第五件事:可变 vs 不可变,该怎么选
这次事故也让我重新思考了"可变集合"和"不可变集合"的取舍。我把两者整理了一下。
| 维度 | 可变集合(ArrayList) | 不可变集合(List.of) |
|---|---|---|
| 能否增删改 | 能 | 不能(改就抛异常) |
| 线程安全 | 否(并发需加锁) | 是(不可变天然线程安全) |
| 被意外修改 | 可能(谁拿到都能改) | 不可能(改不了) |
| 作为常量/配置 | 不安全(可能被改) | ✓ 理想(改不了) |
| 作为返回值 | 调用方可能改你的内部状态 | ✓ 安全(防御性) |
| 适用 | 需要增删改的工作集合 | 常量、配置、返回值、并发共享 |
这张表,让我看到了"不可变"原本是个好东西,关键是用在对的地方。核心区分:需要增删改的"工作集合"用可变(ArrayList);而常量、配置、对外返回值、并发共享的数据,用不可变(List.of)更安全。它给我的最大启发是:"不可变性(immutability)"是一种被低估的、强大的设计工具。不可变集合天然线程安全(没人能改它,自然不存在并发修改问题)、不会被意外篡改(防御性)、作为常量和返回值都很安全。我这次踩坑,虽然起因是"不知道 Arrays.asList 不能 add",但它让我意识到:"不能改"在很多场景下,恰恰是一个特性而非缺陷。这让我领悟到一个更广的设计理念:默认优先使用不可变(immutable)的数据,只在"确实需要修改"时才用可变的;因为不可变的数据"更可预测、更安全、更易于并发"——你不用担心它在你背后被谁改了,也不用担心多线程下的竞态。很多现代语言和框架(函数式编程、React 的不可变状态、Rust 的默认不可变)都在拥抱这个理念。把"可变"当成需要谨慎使用的"特权",把"不可变"当成更安全的"默认"——这种思维转变,能从源头上避开一大类由"共享可变状态"引发的隐蔽 bug。
第六件事:拿到/创建一个集合时,我现在的判断习惯
现在每当我创建或拿到一个集合,我都会先想清楚"它可变吗、我要不要改它":
这张图的精髓,是"用集合前,先确认它的可变性、以及我要不要改它"。第一问 "我需要增删元素吗":只读/遍历的话各种创建方式都行;需要增删再看来源。需要增删时:自己 new 的 ArrayList 可直接用;但工具方法/别人返回/Arrays.asList 的可能固定大小或不可变,要 new ArrayList<>() 包一层拷贝成可变。而对于要长期持有的常量/配置/返回值,优先用 List.of 不可变,更安全。这套习惯,让我用集合时,从"是 List 就以为啥都能干"变成了"先确认它的可变性"——核心始终是:List 是接口,不同实现可变性各异;要改一个来源不明的 List,先包成 new ArrayList 万无一失。
我立下的几条规矩
这场"List 不能 add"的事故,换来了我写 Java 时,刻进骨子里的几条铁律:
- Arrays.asList 返回固定大小的数组视图。可 set 不可 add/remove,add 会抛 UnsupportedOperationException。
- 要可变就 new ArrayList<>(Arrays.asList(...))。拷贝成全新可变列表,和原数组解耦。
- List 是接口,add 是"可选操作"。类型是 List 不保证能 add,不同实现能力天差地别。
- List.of / unmodifiableList 完全不可变。想要常量列表用它们,语义清晰。
- 拿不准又要改,就 new ArrayList 包一层。别假设别人返回/工具创建的 List 可变。
- Arrays.asList 传基本类型数组不拆元素。int[] 会变 List<int[]>,要用包装类型或 stream。
- 优先用不可变,需要才用可变。不可变更安全、线程安全、防意外篡改。
附:一段把各种 List 可变性跑给你看的实验
口说无凭。下面这段代码,把各种方式创建的 List 在 add/set 下的不同表现,一一跑出来:
import java.util.*;
public class ListMutabilityDemo {
static void tryOps(String name, List list) {
System.out.println("--- " + name + " ---");
// 试 set(改已有元素)
try {
list.set(0, "SET");
System.out.println(" set: ✓ 成功");
} catch (UnsupportedOperationException e) {
System.out.println(" set: ✗ UnsupportedOperationException");
}
// 试 add(增加元素)
try {
list.add("ADD");
System.out.println(" add: ✓ 成功");
} catch (UnsupportedOperationException e) {
System.out.println(" add: ✗ UnsupportedOperationException");
}
}
public static void main(String[] args) {
// 1. new ArrayList: 完全可变
tryOps("new ArrayList", new ArrayList<>(List.of("a", "b", "c")));
// 2. Arrays.asList: 固定大小(可set不可add) —— 本文!
tryOps("Arrays.asList", Arrays.asList("a", "b", "c"));
// 3. List.of: 完全不可变
tryOps("List.of", List.of("a", "b", "c"));
// 4. Collections.unmodifiableList: 只读
tryOps("unmodifiableList",
Collections.unmodifiableList(new ArrayList<>(List.of("a", "b"))));
// 5. 正解: new ArrayList 包 Arrays.asList
tryOps("new ArrayList(asList)",
new ArrayList<>(Arrays.asList("a", "b", "c")));
// ====== 额外演示: Arrays.asList 是视图, 改list影响原数组 ======
String[] arr = {"x", "y", "z"};
List view = Arrays.asList(arr);
view.set(0, "CHANGED");
System.out.println("\n改 view 后, 原数组 arr[0] = " + arr[0]); // CHANGED!
}
}
/* 输出:
--- new ArrayList ---
set: ✓ 成功
add: ✓ 成功 ← 完全可变
--- Arrays.asList ---
set: ✓ 成功 ← 能改已有元素
add: ✗ UnsupportedOperationException ← 但不能增! 本文的坑
--- List.of ---
set: ✗ UnsupportedOperationException
add: ✗ UnsupportedOperationException ← 完全不可变
--- unmodifiableList ---
set: ✗ UnsupportedOperationException
add: ✗ UnsupportedOperationException ← 只读
--- new ArrayList(asList) ---
set: ✓ 成功
add: ✓ 成功 ← 正解: 拷贝成可变
改 view 后, 原数组 arr[0] = CHANGED ← asList 是视图, 改list影响原数组!
*/
// 核心: 同样是List, new ArrayList全可变、Arrays.asList可set不可add、List.of/unmodifiable全不可变;
// 跑一遍这张"能力对照表", 各种List到底能干什么就一目了然了。
这段实验代码,把"各种 List 到底能不能改"这件平时容易记混的事,变成了一张亲眼可见的"能力对照表"。它用同一组 set/add 操作,跑遍了五种创建方式,输出清清楚楚地显示:new ArrayList 全可变、Arrays.asList 可 set 不可 add(正是本文的坑)、List.of 和 unmodifiableList 全不可变、而正解 new ArrayList(asList) 又变回全可变;最后还演示了 Arrays.asList 是视图、改 list 会影响原数组那个附加坑。这,正是我想用这段代码,留给每个 Java 开发者的最后一课:当一组相似的东西(各种 List)在某个维度上(可变性)有着"微妙且容易记混的差异"时,把它们放在一起、用同一套操作跑一遍、对比输出,是厘清差异最高效的方式。这种"对照实验"的价值在于:它不是孤立地告诉你"A 是这样的",而是把 A、B、C、D 并排放在一起,让它们的异同在对比中清晰浮现——而"对比",恰恰是人类认知差异、形成记忆最自然的方式。一张这样的"能力对照表",胜过十段孤立的文字描述;而你亲手跑出来的对照表,又胜过别人给你的现成的。把容易混淆的概念,做成一组对照实验跑给自己看——这,是我从这一系列复盘里,总结出的、对付"相似但不同"这类知识点最有效的学习法。从今往后,每当我面对一堆"看着差不多、用起来不一样"的东西,我都会想起这段代码:别死记它们的区别,写个对照实验,让它们自己把区别"跑"给你看。
写在最后
回头看,这场由 Arrays.asList 引发的、List 不能 add 的事故,真正教给我的,远不止"转可变 List 要包 new ArrayList"这一个技巧。它让我对"接口"和"实现"之间那道微妙的鸿沟,有了更清醒的认识。我栽跟头,是因为我把"静态类型"(它声明为 List)当成了"能力的保证"(它一定能做 List 的一切)。可这次事故让我看清:"类型相同"和"能力相同",是两回事;一个变量的静态类型(List),告诉你的是"它看起来能做什么"(有哪些方法可调);而它的运行时实现(Arrays$ArrayList),才决定了"它实际上能做什么"(哪些调用会成功)。这两者之间的差距,正是 UnsupportedOperationException 这类异常诞生的地方。这让我领悟到一个面向对象/接口编程里至关重要的道理:面向接口编程,给了我们"不关心具体实现"的抽象能力,但这种抽象不是免费的——它的代价之一,就是"接口承诺的能力"和"具体实现的真实能力"之间,可能存在我们看不见的差距;而成熟的工程师,既要享受接口抽象带来的灵活,又要时刻对这种"抽象之下的差异"保持警觉。具体到实践,就是:当一个对象的"具体实现/来源"不在我的掌控之中时(别人传来的、工具方法返回的),我不能想当然地假设它具备其接口类型所暗示的全部能力;对它的关键操作,要么先确认它的真实能力,要么(像这次)把它转换成一个我确定其能力的具体实现。看清"接口的承诺"与"实现的现实"之间的距离——这,是我用一次"List 不能 add"的事故,换来的、关于 Java、也关于"接口与实现的鸿沟"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次用 Arrays.asList 后想 add 时,条件反射地先 new ArrayList 包一层,那我对着那个 UnsupportedOperationException 熬的这大半天,就值了。
—— 别看了 · 2026