Java 怎么创建不可变集合?List.of 和 Collections.unmodifiableList 有什么区别?
简化版
不可变集合是「创建后不能再增删改的集合」,好处是线程安全(不可变天然安全)、可安全共享、防意外修改。Java 里创建不可变集合有几种:① List.of()/Set.of()/Map.of()(Java 9+,推荐)——真正的不可变集合,任何修改都抛 UnsupportedOperationException,且不允许 null 元素;② Collections.unmodifiableList(list)——返回原集合的「只读视图」,通过这个视图不能改,但原集合还能改(改了视图也跟着变),所以是「假不可变」(视图不可变,底层可变);③ List.copyOf(list)(Java 10+)——复制一份真不可变的副本(和原集合脱钩)。核心区别:List.of / copyOf 是真不可变(独立、彻底),unmodifiableList 只是只读视图(原集合可变、会联动)。
详细版
三种方式对比:
List<String> src = new ArrayList<>(List.of("a", "b"));
// ① List.of(Java 9+):真不可变集合
List<String> immutable = List.of("a", "b", "c");
immutable.add("x"); // ✗ UnsupportedOperationException
// List.of(null); // ✗ NullPointerException(不允许 null)
// ② Collections.unmodifiableList:只读"视图"(底层还是 src)
List<String> view = Collections.unmodifiableList(src);
view.add("x"); // ✗ 不能通过视图改
src.add("x"); // ✓ 但原集合能改!
System.out.println(view); // [a, b, x]——视图跟着原集合变了!(假不可变)
// ③ List.copyOf(Java 10+):真不可变的副本(和 src 脱钩)
List<String> copy = List.copyOf(src);
src.add("y"); // 改原集合
System.out.println(copy); // 不含 y——copy 是独立副本(真不可变)
对比表:
| 方式 | 是否真不可变 | 和原集合的关系 | null 元素 | 版本 |
|---|---|---|---|---|
List.of(...) | ✅ 真不可变 | 无关(直接创建) | ❌ 不允许 | Java 9+ |
Collections.unmodifiableList(src) | ❌ 只读视图 | 联动(原集合改了视图变) | 看原集合 | 早期 |
List.copyOf(src) | ✅ 真不可变 | 独立副本(脱钩) | ❌ 不允许 | Java 10+ |
⚠️
Collections.unmodifiableList是「视图」不是「副本」——它只是包了一层「禁止通过我修改」的壳,但底层还是原来那个集合。所以「原集合改了,这个『不可变』视图也会跟着变」(假不可变)。如果你想返回一个「外部改不了、也不受原集合影响」的真不可变集合,要用List.of(新建)或List.copyOf(复制副本)。这是「假不可变」最容易踩的坑——以为拿到的是不可变的,结果它悄悄变了。
完整版教学
一、为什么需要不可变集合
不可变集合是「创建后就不能再改(增删改)的集合」,它有几个重要好处:
不可变集合的好处:
① 线程安全:不可变对象天生线程安全(没有写操作,多线程读无需同步)
② 可安全共享:可以放心地把它传给别人/返回给外部,不怕被改
③ 防意外修改:作为常量、配置、方法返回值时,防止被误改
④ 可作为可靠的 key:不可变对象的 hashCode 不变,适合做 Map key
对比可变集合的隐患:
返回一个 ArrayList → 外部可能偷偷 add/remove 它 → 破坏你的内部状态
用可变集合做 Map key → key 被改后 hashCode 变 → 取不出来
不可变集合的核心价值是「安全」——不可变意味着「状态永远不变」,所以线程安全、可共享、可靠。它是「防御性编程」的重要手段:方法返回集合时返回不可变的(防止外部改你的内部数据)、常量集合用不可变的(防止被误改)。这和 String 不可变、Record 不可变是同一个思想(不可变=安全)。理解「不可变集合线程安全、可安全共享、防意外修改」,就理解了它为什么重要——在「共享、并发、防御」的场景很有用。
二、List.of / Set.of / Map.of:真不可变(Java 9+)
Java 9 引入了 List.of()、Set.of()、Map.of() 工厂方法,是创建真不可变集合的现代方式:
List<String> list = List.of("a", "b", "c"); // 不可变 List
Set<String> set = Set.of("a", "b"); // 不可变 Set(不允许重复)
Map<String, Integer> map = Map.of("a", 1, "b", 2); // 不可变 Map
Map<String, Integer> bigMap = Map.ofEntries( // 多于 10 个 entry 用 ofEntries
Map.entry("a", 1), Map.entry("b", 2)
);
// 任何修改都抛异常
list.add("x"); // ✗ UnsupportedOperationException
of 系列的特点:① 真正不可变(任何 add/remove/set 都抛 UnsupportedOperationException);② 不允许 null(List.of(null) 抛 NPE——不可变集合不接受 null,这是有意设计);③ Set.of/Map.of 不允许重复元素/key(重复会抛 IllegalArgumentException);④ 简洁(一行创建)。它是「创建不可变常量集合」的首选——比老式的 Collections.unmodifiableList(Arrays.asList(...)) 简洁得多。理解「List.of/Set.of/Map.of 创建真不可变集合、不允许 null、简洁、Java 9+」,就掌握了现代创建不可变集合的方式。
三、Collections.unmodifiableList:只读视图(假不可变)
Collections.unmodifiableList 是早期创建「不可变」集合的方式,但它是「只读视图」不是「真不可变」——这是关键的坑:
List<String> src = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(src); // 只读视图
view.add("x"); // ✗ 不能通过 view 修改(抛 UnsupportedOperationException)
src.add("x"); // ✓ 但可以改原集合 src!
System.out.println(view); // [a, b, x]——view 跟着 src 变了!
问题的本质:unmodifiableList 只是给原集合包了一层「禁止通过我修改」的壳,但底层引用的还是那个可变的原集合。所以:① 通过 view 不能改(壳挡住了);② 但原集合 src 还能改,改了 view 也跟着变(因为 view 就是 src 的视图)。所以它是「假不可变」——「视图不可变、底层可变」。要让它真正不可变,得保证「没有人持有原集合的引用能改它」(如 Collections.unmodifiableList(new ArrayList<>(src))——先复制一份再包,但这样也繁琐)。理解「unmodifiableList 是只读视图、原集合可变会联动、是假不可变」,就避开了这个高频坑——别以为拿到 unmodifiableList 就万无一失。
四、List.copyOf:真不可变副本(Java 10+)
List.copyOf(Java 10+)解决了 unmodifiableList 的「假不可变」问题——它复制一份真不可变的独立副本:
List<String> src = new ArrayList<>(List.of("a", "b"));
List<String> copy = List.copyOf(src); // 复制一份真不可变副本
src.add("x"); // 改原集合
System.out.println(copy); // [a, b]——copy 不受影响(独立副本)
copy.add("y"); // ✗ UnsupportedOperationException(真不可变)
List.copyOf 的特点:① 复制独立副本(和原集合脱钩,原集合改了它不变);② 真不可变(副本本身不能改);③ 不允许 null。它是「从一个现有集合,创建一个真不可变副本」的最佳方式——比 Collections.unmodifiableList(new ArrayList<>(src)) 简洁、意图清晰。一个小优化:如果 src 本身已经是不可变集合(如 List.of 创建的),copyOf 会直接返回它(不复制,省内存)。
选择:
从零创建不可变集合 → List.of("a", "b")
从现有集合创建真不可变副本 → List.copyOf(src)
只想要个只读视图(且知道底层会变、就想要联动)→ Collections.unmodifiableList
理解「List.copyOf 复制真不可变独立副本、和原集合脱钩、Java 10+」,就掌握了「从现有集合创建真不可变」的正确方式。
五、真不可变 vs 视图不可变的本质
把三种方式的「不可变性」本质对比清楚,是这道题的核心:
"真不可变"(List.of / List.copyOf):
集合本身就是不可变的(内部数据不能改)
没有任何途径能修改它(自己不能改、也不受外部影响)
→ 彻底安全、可放心共享
"视图不可变"(Collections.unmodifiableList):
只是"禁止通过这个视图修改",但底层集合还可变
→ 如果有人持有底层集合的引用并修改,视图会跟着变
→ "假不可变":看起来不可变,实则底层可变
关键区别:真不可变是"数据本身不可变"、视图不可变是"这个入口不能改但数据可能被别的入口改"
一个类比:真不可变像「刻在石头上的字」(谁也改不了);视图不可变像「戴了手套摸屏幕」(你摸不动,但别人能直接操作屏幕,屏幕内容会变)。所以「视图不可变」提供的安全性是有限的——只有在「确保没人持有底层集合的可变引用」时才真安全。这也是为什么现代 Java 推荐 List.of/copyOf(真不可变)而非 unmodifiableList(视图)。理解「真不可变是数据本身不可变(安全彻底)、视图不可变是入口只读但底层可变(假不可变有隐患)」,就抓住了这道题的精髓。
六、浅不可变的边界
还有一个进阶点——Java 的不可变集合都是「浅不可变(shallow immutable)」:
"浅不可变"的含义:
不可变集合保证"集合结构不可变"(不能增删元素、不能替换元素)
但"元素对象本身"如果是可变的,元素的内部状态还能改!
List<StringBuilder> list = List.of(new StringBuilder("a"));
list.add(...); // ✗ 不能增删元素(集合不可变)
list.get(0).append("b"); // ✓ 但能改元素对象的内部状态!(StringBuilder 是可变的)
// list 里的 StringBuilder 现在是 "ab" 了
所以:不可变集合 ≠ 里面的元素也不可变
→ 要彻底不可变,元素本身也得是不可变的(如 String、Integer、Record)
关键认知:不可变集合只保证「集合层面不可变」(不能增删改元素引用),不保证「元素对象本身不可变」。如果元素是可变对象(StringBuilder、可变的自定义类),你还能改元素的内部状态。要「深不可变」,得让元素也是不可变的(用 String、Record 等不可变类型做元素)。这和「final 修饰引用只保证引用不变、对象内容可变」是同一个道理(前面 final 题讲过)。理解「不可变集合是浅不可变、元素对象本身若可变仍能改、要深不可变需元素也不可变」,就补齐了不可变集合的完整认知——别以为不可变集合里的一切都冻结了。
记忆钩子:「不可变集合线程安全/可共享/防误改;List.of/Set.of/Map.of(Java 9+)是真不可变(改抛 UnsupportedOperationException、不允许 null、简洁);Collections.unmodifiableList 是只读视图=假不可变(原集合改了它联动变、只挡住通过视图改);List.copyOf(Java 10+)是真不可变独立副本(和原集合脱钩);都是浅不可变(集合结构不可变但可变元素的内部状态仍能改,要深不可变需元素也不可变)」。
七、常见误区与追问
- 误区:Collections.unmodifiableList 是真不可变。 是只读视图(假不可变)——只挡住「通过这个视图修改」,底层原集合还能改,改了视图会联动变;真不可变用 List.of 或 List.copyOf。
- 误区:List.of 可以放 null。 不允许——
List.of(null)抛 NPE;不可变集合有意不接受 null(Set.of/Map.of 还不允许重复元素/key)。 - 误区:不可变集合里的元素也都不可变。 是浅不可变——集合结构不可变(不能增删改元素引用),但如果元素是可变对象(StringBuilder),元素的内部状态还能改;要深不可变需元素本身也不可变。
- 误区:unmodifiableList 和 copyOf 一样。 不同——unmodifiableList 是原集合的只读视图(联动、假不可变),copyOf 是独立的真不可变副本(脱钩、真不可变)。
- 追问:List.of 和 Collections.unmodifiableList 的区别? List.of 直接创建真不可变集合(独立、不允许 null、Java 9+);unmodifiableList 返回原集合的只读视图(原集合改了它联动变、是假不可变),要真不可变副本用 List.copyOf。
- 追问:怎么从一个现有集合创建真不可变集合? 用 List.copyOf(src)(Java 10+,复制独立的真不可变副本、和原集合脱钩);别用 Collections.unmodifiableList(那是只读视图、会联动)。
- 追问:不可变集合是深不可变还是浅不可变? 浅不可变——保证集合结构不可变(不能增删改元素引用),但不保证元素对象本身不可变(可变元素的内部状态仍能改);要深不可变需元素也用不可变类型。
八、加强记忆
不可变集合是「创建后不能再增删改」的集合,好处是线程安全、可安全共享、防意外修改(不可变=安全,同 String/Record)。三种创建方式的关键区别在「真不可变 vs 视图不可变」:① List.of/Set.of/Map.of(Java 9+) 是真不可变(任何修改抛 UnsupportedOperationException、不允许 null、简洁、直接创建、独立);② Collections.unmodifiableList(src) 是只读视图=假不可变(只挡住「通过这个视图改」,底层原集合还能改、改了视图联动变——这是高频坑);③ List.copyOf(src)(Java 10+) 是真不可变的独立副本(复制一份、和原集合脱钩、原集合改了它不变)。选择:从零创建用 List.of、从现有集合创建真不可变副本用 List.copyOf、只要只读视图(接受联动)才用 unmodifiableList。另外所有不可变集合都是浅不可变——集合结构不可变(不能增删改元素引用),但可变元素对象的内部状态仍能改(要深不可变需元素本身也是不可变类型如 String/Record)。一句话「List.of/copyOf 真不可变(独立、不允许 null)、unmodifiableList 是只读视图假不可变(原集合改了联动)、都是浅不可变(可变元素内部仍可改)」。