List视图与不可变

1) 先分清三层概念

  • 视图(view) :不拷贝数据、共享底层存储 的"窗口"。对视图或其"父集合"的结构性修改会彼此可见 ,并受 fail-fast 影响(易抛 ConcurrentModificationException)。

  • 不可变的两层含义

    1. unmodifiable(不可修改) :集合接口上的增删改操作直接 UnsupportedOperationException,但底层若被别人改,你仍会看到变化(浅不可变)。

    2. 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) 实战最佳实践

  1. 对外返回集合

    • Java:return Collections.unmodifiableList(new ArrayList<>(internal));(先拷贝再只读包装)。
    • Kotlin:fun expose(): List = internal.toList();需要强不可变则 toPersistentList()。
  2. 只用视图做短期操作(排序/清理一段)

    • list.subList(i, j).clear() 或 Collections.sort(list.subList(i, j)),用完即丢,不要长持。
  3. 避免 Arrays.asList 当可变表

    • 需要增删:new ArrayList<>(Arrays.asList(...));或 array.toMutableList()(Kotlin)。
  4. 团队接口契约

    • 输入参数 :若函数需要保留快照,立即拷贝(防止调用方后续修改影响内部)。
    • 返回值:约定"只读"还是"真正不可变";并在文档中说明是否会随内部变化而变化。
  5. 并发读写

    • 避免"视图 + 另处写"组合;需要并发读多写少: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 或持久化集合;需要强一致靠同步或消息化,别指望"只读视图"解决并发一致性。
相关推荐
2601_949950633 小时前
练题簿在线免费刷题 从资料整理到考前自测
面试·职场和发展·pdf·word·刷题
时间的拾荒人4 小时前
Qt 多线程详解:从 QThread 到实战
开发语言·qt·面试
海绵宝宝转agent4 小时前
MySql高频面试八股开源笔记总结
mysql·面试·开源
小呆呆6665 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
lsc_blog6 小时前
远程和居家办公的经历,技术简历上怎么摆
面试·职场和发展·重构·pdf·求职招聘
时间的拾荒人7 小时前
Qt 事件机制全解析:从事件处理到事件过滤器
开发语言·qt·面试
(Charon)8 小时前
【C++面试】vector底层原理:扩容、拷贝移动与迭代器失效
c++·面试
考研保研资料分享9 小时前
电气工程保研经验:夏令营失利后上岸上交,科研追问与九推复习(2024)
面试
时间的拾荒人10 小时前
Qt 窗口系统详解:从菜单栏到对话框的完整指南
开发语言·qt·面试
无忧.芙桃11 小时前
位图妙用:512MB 实现 40 亿整数的存在性查询
c语言·c++·面试·哈希算法