sublist返回的是原列表的活视图而非副本,不复制数据,结构性修改双向影响,易因原列表变更导致索引越界;安全用法是显式复制或包装为不可修改列表。

subList 返回的是视图,不是副本
调用 list.subList(fromIndex, toIndex) 得到的不是新列表,而是一个指向原 ArrayList 底层数组的“活视图”。它不复制数据,只维护逻辑区间。这意味着:对子列表做结构性修改(如 add()、remove()、clear()),会直接改写原列表;反过来,原列表的结构性修改(比如 list.remove(0))也会让子列表的索引映射错乱,甚至抛出 IndexOutOfBoundsException。
- 常见错误现象:
sub.clear()后发现原列表前几项没了——这不是 bug,是设计使然 - 使用场景:适合临时切片读取、配合
Collections.unmodifiableList()做受限访问,但绝不适合需要独立生命周期的子集 - 性能影响:零拷贝,内存友好;但副作用强,调试时容易误判数据丢失位置
原集合增删导致 subList 索引越界
subList 的底层仍依赖原数组的物理索引。一旦原列表在子视图创建后发生结构性变更(比如在子区间之前插入元素),子视图内部的 fromIndex 和 toIndex 就不再对应真实位置。下次调用 sub.get(0) 或 sub.size() 时,可能触发 IndexOutOfBoundsException,尤其在多线程或复杂业务流程中难以复现。
- 典型触发链:创建
sub = list.subList(2, 5)→list.add(0, "x")→sub.get(0)报错 - 参数差异:
subList的起止索引是“快照式”的,不随原列表动态调整 - 兼容性注意:JDK 所有版本行为一致,不是版本迁移问题,而是 API 语义本身如此
如何安全使用 subList
如果真需要一个独立子集,就别依赖 subList 的视图语义——它本就不是为隔离修改而生的。要么明确接受其副作用,要么主动转成副本。
- 读多写少场景:用
new ArrayList(list.subList(a, b))显式复制,代价小且语义清晰 - 需写入但不想影响原列表:必须复制,
subList+add()是危险组合 - 权限控制场景:可包装为
Collections.unmodifiableList(list.subList(a, b)),禁止任何修改,避免误操作 - 绝对不要:把
subList存进缓存、跨方法传递、或在循环中反复调用subList而不检查原列表是否被改过
为什么 Java 不修复这个“反直觉”设计
因为 subList 的视图语义是刻意为之:它要支持 O(1) 切片、零内存分配、与原列表实时同步(比如日志滚动窗口)。修复等于重定义行为,破坏所有依赖该语义的旧代码。Java 集合框架把“责任”交给了使用者——你拿到的是视图,就得按视图的规则来。
最容易被忽略的一点:哪怕你只读 subList,只要原列表在别处被结构性修改,下一次调用 sub.size() 或 sub.iterator().hasNext() 仍可能失败。这不是并发问题,是单线程下也存在的结构耦合。










