我用 Arrays.asList 把数组转成 List 然后往里 add 元素,运行时抛了 UnsupportedOperationException,代码编译明明全过,我对着这个"只读 List"排查了大半天的复盘

一段很普通的 Java 代码:用 Arrays.asList() 把数组转成 List,然后往里 add 几个元素,编译器全程绿灯,一运行却抛 java.lang.UnsupportedOperationException。满脸问号:类型明明是 List,List 不就该能 add 吗?编译都过了凭什么运行时说不支持?排查大半天才理解 Arrays.asList() 返回的根本不是 java.util.ArrayList,而是 Arrays 类内部的私有静态内部类(同名但完全不同),它是对原数组的一层包装(视图):底层直接就是你传进去的数组、长度固定;所以支持 get/set(读和改已有元素,不变长度),但不支持 add/remove/clear(要变长度,而数组长度固定),这些方法没重写、用父类 AbstractList 默认实现直接抛异常;更坑的是它是视图,改 list 会影响原数组。这篇从 List 是接口/add 是可选操作/不同实现可变性天差地别、要可变就 new ArrayList 包一层拷贝/只读用 asList/不可变用 List.of 的正解、各种创建 List 方式可变性速查、可变 vs 不可变怎么选、决策图与铁律,到附上一段把各种 List 可变性跑给你看的对照实验。核心领悟:类型相同不等于能力相同,静态类型告诉你它看起来能做什么、运行时实现才决定它实际能做什么,面向接口编程要警惕接口承诺与实现现实的差距;优先用不可变更安全;把相似但不同的概念做成对照实验跑给自己看是最高效的学习法。

我用 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 ArrayListnew ArrayList(otherCollection)new LinkedList固定大小(可 set 不可增删):Arrays.asList(本文,数组视图)完全不可变:List.ofList.copyOfCollections.unmodifiableListemptyListsingletonListtoUnmodifiableList判断口诀: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 时,刻进骨子里的几条铁律:

  1. Arrays.asList 返回固定大小的数组视图。可 set 不可 add/remove,add 会抛 UnsupportedOperationException。
  2. 要可变就 new ArrayList<>(Arrays.asList(...))。拷贝成全新可变列表,和原数组解耦。
  3. List 是接口,add 是"可选操作"。类型是 List 不保证能 add,不同实现能力天差地别。
  4. List.of / unmodifiableList 完全不可变。想要常量列表用它们,语义清晰。
  5. 拿不准又要改,就 new ArrayList 包一层。别假设别人返回/工具创建的 List 可变。
  6. Arrays.asList 传基本类型数组不拆元素。int[] 会变 List<int[]>,要用包装类型或 stream。
  7. 优先用不可变,需要才用可变。不可变更安全、线程安全、防意外篡改。

附:一段把各种 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.ofunmodifiableList 全不可变、而正解 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的 Go 程序往一个 struct 里的 map 字段写值就 panic,可同一个 struct 里的 slice 字段 append 却好好的,我对着 nil map 排查了大半天的复盘

2026-6-2 7:28:49

技术教程

用户昵称带个 emoji 就插入数据库失败、报 Incorrect string value,我数据库明明设的是 utf8 字符集,我对着 MySQL 的"假 utf8"排查了大半天的复盘

2026-6-2 7:40:16

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索