subList 返回原List的动态视图而非新列表,结构性修改可能抛UnsupportedOperationException或影响原列表,原列表修改可能导致ConcurrentModificationException;需副本时应显式new ArrayList(subList())。

subList 返回的是视图,不是新 List
调用 subList 得到的不是一份独立拷贝,而是原 List 的一个动态视图。这意味着:对子列表的结构性修改(如 add、remove、clear)会直接抛出 UnsupportedOperationException(除非底层实现支持,比如 ArrayList 允许通过子列表删除元素,但会同步影响原列表);而对原列表的修改,也可能让子列表抛出 ConcurrentModificationException。
常见错误现象:subList.remove(0) 在某些 JDK 版本或实现上看似成功,但其实是调用了原列表的 remove,导致索引偏移;更危险的是,后续再访问该子列表可能触发 fail-fast 机制。
- 如果需要可修改的副本,必须显式构造:
new ArrayList(list.subList(from, to)) - 如果只是读取或遍历,视图是轻量且安全的,无额外内存开销
-
subList的起始索引包含,结束索引不包含(左闭右开),这点和String.substring一致
边界检查由 subList 自动执行,但异常信息不提示具体越界值
subList(fromIndex, toIndex) 内部会校验 fromIndex 和 toIndex 是否合法:要求 0 ≤ fromIndex ≤ toIndex ≤ size()。一旦不满足,抛出 IndexOutOfBoundsException,但 JDK 8/11/17 的错误信息通常只写 “fromIndex = X, toIndex = Y”,并不说明当前 size() 是多少,容易在调试时多绕一圈。
- 建议在调用前自行检查:
if (toIndex > list.size()) toIndex = list.size();(适用于截取“最多 N 个”的场景) - 注意:空列表调用
subList(0, 0)是合法的,返回空视图;但subList(0, 1)会直接失败 - 使用
Math.min(toIndex, list.size())比 try-catch 更高效且语义清晰
subList 在 ArrayList 和 LinkedList 上性能差异极大
subList 本身是 List 接口方法,所有实现类都必须提供。但底层行为不同:ArrayList.subList 是 O(1) 时间创建视图,后续 get(i) 是 O(1);而 LinkedList.subList 虽然也返回视图,但每次 get(i) 都需从头或尾遍历定位,最坏 O(n),且子列表的 iterator() 也会反复跳链表节点。
- 若源列表是
LinkedList,又频繁随机访问子列表,实际性能可能比直接遍历原列表还差 - 没有“子链表优化”——
LinkedList.subList并不会构造新链表或缓存首尾节点 - 实践中,除非确定只顺序遍历且源列表确实是
LinkedList(已很少见),否则优先用ArrayList+subList
并发环境下 subList 视图完全不可靠
subList 不提供任何线程安全保证。即使原列表是 CopyOnWriteArrayList,其 subList 返回的对象也不是线程安全的 —— 因为它内部仍持有对原始数组的引用,且迭代器不加锁。
- 多线程读+单线程写也不安全:写操作可能触发数组替换,子列表持有的旧引用会导致
ArrayIndexOutOfBoundsException或数据错乱 - 不要试图用
Collections.synchronizedList包装后再调用subList:同步块不覆盖子列表方法,各线程仍可并发调用子列表的get等方法 - 真正需要并发安全的分段访问,应改用
Queue分片、显式复制,或基于StampedLock手动控制读写范围
视图的“廉价”是有前提的:它省了内存,但把复杂性转移到了行为一致性、边界安全和并发模型上。最容易被忽略的一点是——很多人以为 subList 可以替代切片工具函数,却没意识到它连基本的“防越界兜底”都不做,更不承诺线程行为。










