1) 先分清三层概念
-
视图(view) :不拷贝数据、共享底层存储 的"窗口"。对视图或其"父集合"的结构性修改会彼此可见 ,并受 fail-fast 影响(易抛 ConcurrentModificationException)。
-
不可变的两层含义
-
unmodifiable(不可修改) :集合接口上的增删改操作直接 UnsupportedOperationException,但底层若被别人改,你仍会看到变化(浅不可变)。
-
immutable/persistent(真正不可变/持久化) :结构性不可变 ,每次"修改"都会返回新集合(结构共享),旧集合永远不变,天然线程安全(结构层面)。
-
2) 常见"视图"类型与特性
-
List.subList(from, to) :区间视图
- 共享父 List 的存储与 modCount;对子区间做 set/clear/sort 直接作用在父表相应区间。
- 与父表交叉修改易触发 fail-fast:先 subList.clear() 再对父表 add(),再访问 subList → 可能抛 CME。
- 内存风险 :长时间持有 subList 可能间接保住整个父表 (尤其父是大 ArrayList),导致难以释放。需要独立使用时请 new ArrayList<>(subList) 做快照。
-
Arrays.asList(array) :数组视图 (定长)
- 底层是原生数组的包装 ,允许 set(i,x),但 不允许 add/remove(会抛异常)。
- 数组与 List 同步变化:改数组会反映到 List,反之亦然(限 set)。
- 常见坑:Arrays.asList(int[]) 会被当成一个元素 ;要使用 装箱类型 或 IntStream.boxed()。
-
Collections.unmodifiableList(list) :不可修改的视图
- 只是包了一层只读代理 ;若底层 list 被别人改,该"只读视图"看到的内容也会变(浅不可变)。
- 读多团队共享时,除非保证没人能持有原可变引用,否则不是严格意义的"不可变"。
-
Collections.synchronizedList(list) :加锁视图
-
对单次方法调用做内部同步;遍历时仍需手动 synchronized(list){...} 。
-
是"并发包装",不是不可变;仍共享底层。
-
3) 真正意义的"不可变"集合
-
Java 9+ List.of(...) / List.copyOf(...) :返回不可修改的集合实现 ;不接受 null;不会随外部变化;copyOf 会制作不可变副本。
-
Guava ImmutableList :构建后永久不可变(浅不可变,元素本身若可变仍可变)。
-
Kotlin 持久化集合 :kotlinx.collections.immutable 的 persistentListOf()/toPersistentList(),结构共享 + 每次修改返回新实例,JVM/Android 可用,适合并发读多写少。
术语小结:unmodifiable = 不允许通过这个引用改;immutable/persistent = 结构真的不可变且修改返回新对象。
4) Kotlin 专项:ListvsMutableList
-
接口层面 :List 是只读接口 ,MutableList 可变;是否真不可变取决于实现 。把一个 MutableList 赋给 List 变量,只是读接口受限 ,若仍持有原 MutableList 引用,底层仍会变。
-
构建建议****
-
需要"读接口 + 不被外部意外改":val ro: List = someMutableList.toList()(拷贝一个新数组)。
-
需要真正结构不可变:用 persistentListOf() 或 toPersistentList()。
-
listOf(...) 返回只读视图语义的实现;不要依赖强制 cast 去写入。
-
5) 并发、fail-fast 与视图的交互
-
视图共享底层 → 跨引用修改很容易让 modCount 不一致;随后对任一视图迭代就可能 CME。
-
unmodifiable 视图 只是不许你从这个引用改;别人改了底层你仍然"看得到变化",也可能在迭代时被 fail-fast。
-
persistent/immutable 集合 没有这个问题:每次"修改"都产出新结构,老结构永远一致(弱点是修改时会分配新对象,但共享大部分结构,复杂度通常是 O(log n))。
6) 性能与内存对照
类型 | 是否拷贝 | 修改成本 | 迭代安全 | 适用场景 |
---|---|---|---|---|
subList 视图 | ❌(共享) | 改子区间 O(k) | 易受 fail-fast 影响 | 区间处理、局部排序、临时窗口 |
Arrays.asList 视图(定长) | ❌(共享) | 仅 set | 不改结构即可 | 适配旧 API、快速包装 |
unmodifiableList 视图 | ❌(共享) | 禁改 | 受底层影响 | 对外只读暴露(需确保外部无原引用) |
List.of/ImmutableList | ✅(构建时拷贝) | 不可改 | 结构稳定 | 配置/常量/并发读 |
Kotlin persistentList | 结构共享(局部节点新建) | 修改返回新对象 | 结构稳定 | 并发/快照、多版本回溯 |
7) 实战最佳实践
-
对外返回集合
- Java:return Collections.unmodifiableList(new ArrayList<>(internal));(先拷贝再只读包装)。
- Kotlin:fun expose(): List = internal.toList();需要强不可变则 toPersistentList()。
-
只用视图做短期操作(排序/清理一段)
- list.subList(i, j).clear() 或 Collections.sort(list.subList(i, j)),用完即丢,不要长持。
-
避免 Arrays.asList 当可变表
- 需要增删:new ArrayList<>(Arrays.asList(...));或 array.toMutableList()(Kotlin)。
-
团队接口契约
- 输入参数 :若函数需要保留快照,立即拷贝(防止调用方后续修改影响内部)。
- 返回值:约定"只读"还是"真正不可变";并在文档中说明是否会随内部变化而变化。
-
并发读写
-
避免"视图 + 另处写"组合;需要并发读多写少:CopyOnWriteArrayList(快照迭代、不抛 CME)或持久化集合。
-
需要强一致:外部同步或消息化(避免共享可变集合)。
-
8) 代码模版
A. 安全对外暴露(Java)
swift
public final class Repo {
private final List<Item> items = new ArrayList<>();
public List<Item> snapshot() {
return Collections.unmodifiableList(new ArrayList<>(items)); // 拷贝 + 只读
}
}
B. Kotlin:只读或真正不可变
kotlin
class Repo {
private val _items = mutableListOf<Item>()
fun readOnly(): List<Item> = _items.toList() // 拷贝快照
fun immutable(): PersistentList<Item> = _items.toPersistentList() // 真正不可变
}
C. subList 用后即焚
ini
List<Item> window = list.subList(100, 200);
Collections.sort(window, cmp);
window.clear(); // 清理区间
// 之后不要再持有 window 变量
D. Arrays.asList 正确姿势
ini
String[] arr = {"a", "b"};
List<String> fixed = Arrays.asList(arr); // 定长
fixed.set(0, "A"); // OK,反映回 arr
// fixed.add("c"); // ❌ UnsupportedOperationException
List<String> growable = new ArrayList<>(fixed); // 可增删
TL;DR(面试/落地小抄)
- subList/Arrays.asList/unmodifiableList 都是"视图" :共享底层,不等于真正不可变,会受 fail-fast 约束。
- 需要稳定不变 :Java 用 List.of/copyOf 或 Guava ImmutableList;Kotlin 用 persistentList。
- 对外暴露集合 :拷贝 + 只读包装 是默认安全做法;视图只做短期操作。
- 并发场景:读多写少用 CopyOnWriteArrayList 或持久化集合;需要强一致靠同步或消息化,别指望"只读视图"解决并发一致性。