List.subList 返回的是什么?为什么修改 subList 会影响原列表?
简化版
list.subList(from, to) 返回的不是一个新的独立列表,而是原列表的「视图(view)」——它只是「原列表 [from, to) 这一段的一个窗口」,底层数据还是原列表的。所以:① 修改 subList 会影响原列表(在 subList 上 set、add、remove 都会作用到原列表对应位置);② 修改原列表也会影响 subList(甚至可能让 subList 失效)。这带来几个坑:① 想要独立副本要用 new ArrayList<>(list.subList(from, to))(复制一份);② subList 后如果对原列表做结构性修改(add/remove),再用 subList 会抛 ConcurrentModificationException。一句话:subList 是视图不是副本,和原列表双向联动,要独立副本得自己复制。
详细版
subList 是视图,双向联动:
List<Integer> list = new ArrayList<>(List.of(0, 1, 2, 3, 4, 5));
List<Integer> sub = list.subList(1, 4); // 视图:[1, 2, 3](索引 1~3)
// ① 改 subList 影响原列表
sub.set(0, 99); // 改 sub 的第 0 个(即原列表索引 1)
System.out.println(list); // [0, 99, 2, 3, 4, 5]——原列表变了!
// ② 在 subList 上删除也影响原列表
sub.remove(0); // 删 sub 的第 0 个
System.out.println(list); // [0, 2, 3, 4, 5]——原列表也删了!
// ③ 改原列表也影响 subList(甚至失效)
list.set(2, 88); // 改原列表索引 2
System.out.println(sub); // sub 也跟着变
经典坑:结构性修改后 subList 失效:
List<Integer> list = new ArrayList<>(List.of(0, 1, 2, 3, 4));
List<Integer> sub = list.subList(1, 3); // [1, 2]
list.add(5); // ★ 对原列表做结构性修改(add)
sub.get(0); // ✗ ConcurrentModificationException!
// 因为 subList 检测到原列表的 modCount 变了
要独立副本就复制:
// ✓ 想要一个独立的、不影响原列表的子列表 → 复制一份
List<Integer> independent = new ArrayList<>(list.subList(1, 4));
independent.set(0, 99); // 只改 independent,不影响原列表
⚠️
subList后对原列表做「结构性修改」(add/remove 等改变列表大小的操作)会让 subList 失效——再操作 subList 抛ConcurrentModificationException。原因和 fail-fast 一样:subList 视图记录了原列表的 modCount,原列表结构变了(modCount 变),subList 检测到不一致就抛异常。所以「subList 后别对原列表做结构性修改,除非通过 subList 做」。要避免这些坑,最简单的办法是「subList 后立即复制成新 ArrayList」。
完整版教学
一、subList 是视图不是副本
subList 最核心的认知是——它返回的是原列表的「视图(view)」,不是一个独立的新列表:
subList(from, to) 返回什么:
不是:把 [from, to) 这段元素复制到一个新 ArrayList
而是:一个"视图对象",它内部持有"原列表的引用"和"from/to 的偏移"
对这个视图的操作,会被"翻译"成对原列表对应位置的操作
所以 subList 和原列表"共享底层数据":
sub.get(i) → 实际访问 list.get(from + i)
sub.set(i,x) → 实际执行 list.set(from + i, x)
→ 改视图就是改原列表、改原列表也会反映到视图
关键理解:subList 是原列表的「一个窗口」,透过这个窗口看到和操作的是原列表的一段数据。它不复制数据(省内存、创建快),但代价是「和原列表双向联动」。这和前面「Arrays.asList 是数组视图」「unmodifiableList 是只读视图」是同一类「视图」概念——视图不拥有数据,只是数据的一个入口。理解「subList 是视图不是副本、和原列表共享底层数据、双向联动」,就抓住了它的本质,后面的坑都是这个本质的后果。
二、修改的双向联动
因为是视图,subList 和原列表的修改是双向联动的:
List<Integer> list = new ArrayList<>(List.of(0, 1, 2, 3, 4, 5));
List<Integer> sub = list.subList(1, 4); // 视图 [1, 2, 3]
// 方向1:改 subList → 影响原列表
sub.set(0, 99); // list 变成 [0, 99, 2, 3, 4, 5]
sub.remove(0); // list 变成 [0, 2, 3, 4, 5](原列表也删了元素)
sub.add(88); // 在 sub 末尾加,会插入到原列表对应位置
// 方向2:改原列表 → 影响 subList(非结构性修改)
list.set(2, 77); // sub 也跟着变
两个方向:① 改 subList 影响原列表(set/add/remove/clear 都作用到原列表对应位置——如 subList(from, to).clear() 会删除原列表 [from, to) 这段,这其实是个有用的技巧);② 改原列表影响 subList(原列表 set 后 subList 也变)。这个联动既是特性也是坑:特性——可以用 list.subList(from, to).clear() 高效删除一段;坑——不小心改了 subList 却影响了原列表(或反之)。理解「subList 和原列表双向联动、改一个影响另一个」,就知道用它时要小心「意外的相互影响」。
三、经典坑:结构性修改导致 CME
最容易踩的坑是——subList 后对原列表做「结构性修改」(改变大小的 add/remove),再用 subList 会抛 ConcurrentModificationException:
List<Integer> list = new ArrayList<>(List.of(0, 1, 2, 3, 4));
List<Integer> sub = list.subList(1, 3); // [1, 2]
list.add(5); // ★ 对原列表结构性修改(add,大小变了)
sub.get(0); // ✗ ConcurrentModificationException!
原因(和 fail-fast 迭代器同理):
subList 视图创建时,记录了原列表当时的 modCount(expectedModCount)
subList 的每个操作都检查:原列表的 modCount == 我记录的 expectedModCount 吗?
list.add(5) → 原列表 modCount +1(结构性修改)
但 subList 的 expectedModCount 没变
→ 下次操作 subList 时检查发现 modCount != expectedModCount → 抛 CME
核心:subList 视图假设「原列表在我存在期间结构不变」,一旦原列表被结构性修改(add/remove 改变大小),视图就『失效』了(抛 CME)。因为原列表大小变了,视图的 [from, to) 范围可能就不对了(数据错位),所以 Java 用 fail-fast 及时报错。注意:非结构性修改(set 只改值不改大小)不会触发 CME,只有 add/remove 这类结构性修改会。理解「subList 后对原列表结构性修改会让 subList 失效抛 CME(modCount 检测)」,就避开了这个高频坑。
四、正确用法:要独立副本就复制
既然 subList 是视图、有这么多联动的坑,很多时候我们其实想要「一个独立的、不和原列表联动的子列表」——这时要复制一份:
// ✗ 想要独立子列表,但直接用 subList → 会和原列表联动、有坑
List<Integer> sub = list.subList(1, 4); // 视图,联动
// ✓ 要独立副本 → 用 subList 的结果构造一个新 ArrayList(复制)
List<Integer> independent = new ArrayList<>(list.subList(1, 4));
// 现在 independent 是独立的:
independent.set(0, 99); // 不影响原列表
list.add(100); // 不影响 independent(不会 CME)
「new ArrayList<>(list.subList(from, to))」的作用是「把视图的内容复制到一个新的独立 ArrayList」——新列表和原列表完全脱钩,改任一个都不影响另一个,也不会因原列表结构修改而 CME。这是「取一段数据、且要独立使用」的正确姿势。判断标准:如果只是「临时操作原列表的一段」(如清空一段 subList(from,to).clear())→ 直接用 subList 视图;如果要「拿到一段数据独立使用」→ 复制成新 ArrayList。理解「要独立子列表就 new ArrayList 复制 subList、避免视图联动和 CME」,就掌握了 subList 的正确用法。
五、subList 的有用场景
虽然有坑,但 subList 的「视图」特性也有有用的场景——尤其是「批量操作原列表的一段」:
// 有用场景1:高效删除一段(视图的 clear 会删原列表对应段)
List<Integer> list = new ArrayList<>(List.of(0, 1, 2, 3, 4, 5));
list.subList(1, 4).clear(); // 删除原列表索引 1~3 → list 变 [0, 4, 5]
// 比逐个 remove 高效、简洁
// 有用场景2:对一段做批量操作(因为联动,操作视图就是操作原列表那段)
Collections.sort(list.subList(2, 5)); // 只排序原列表的 [2,5) 这一段
Collections.reverse(list.subList(0, 3)); // 只反转前 3 个
// 有用场景3:只读地查看一段(不修改的话很安全,也省内存)
process(list.subList(0, 10)); // 处理前 10 个(不复制、省内存)
subList 视图的价值是「零拷贝地操作/查看原列表的一段」——不用复制数据(省内存、快),直接对原列表的某段做批量操作(清空、排序、反转、只读查看)。经典技巧是 list.subList(from, to).clear() 高效删除一段(比循环 remove 简洁高效)。所以 subList 不是「不能用」,而是「用对场景」——临时操作原列表的一段用视图(省拷贝),要独立数据用复制。理解「subList 视图适合零拷贝操作/查看原列表一段(如 clear 删段、排序一段)」,就知道它的正确用武之地——它的「联动」在这些场景下反而是优点。
六、和其他视图的对比
subList 是「视图」,Java 集合里还有几个「视图」,一起理解「视图」这个概念:
| 视图 | 来源 | 特性 |
|---|---|---|
list.subList(from, to) | List 的一段 | 双向联动、结构性修改会 CME |
Arrays.asList(arr) | 数组 | 和数组联动、定长(不能 add/remove) |
Collections.unmodifiableList(list) | 原列表 | 只读、原列表改了联动变 |
map.keySet()/values()/entrySet() | Map | Map 改了视图变、改视图影响 Map |
这些「视图」的共同特点是「不拥有数据、只是数据的一个入口/窗口,和源数据联动」。理解「视图」这个概念很重要——很多「改了 A 结果 B 也变了」的诡异现象,都是因为「A 和 B 其实是同一份数据的不同视图」。map.keySet() 也是视图(在 keySet 上 remove 会删 Map 的对应 entry)。所以看到「返回一个集合」时要问:这是「副本」还是「视图」? 副本独立、视图联动。理解「subList/asList/unmodifiableList/keySet 都是视图、和源数据联动、要独立得复制」,就把 Java 集合的「视图」概念系统掌握了。
记忆钩子:「subList(from,to) 返回视图不是副本——和原列表共享底层数据、双向联动(改 subList 影响原列表、改原列表影响 subList);坑:subList 后对原列表结构性修改(add/remove)会让 subList 失效抛 ConcurrentModificationException(modCount 检测);要独立子列表用 new ArrayList<>(list.subList(…)) 复制;有用场景:subList(from,to).clear() 高效删段、排序/反转一段(零拷贝);Arrays.asList/unmodifiableList/keySet 也都是视图」。
七、常见误区与追问
- 误区:subList 返回一个新的独立列表。 返回的是原列表的「视图」——共享底层数据、和原列表双向联动;要独立列表得
new ArrayList<>(list.subList(...))复制。 - 误区:改 subList 不会影响原列表。 会——subList 是视图,在它上面 set/add/remove/clear 都作用到原列表对应位置(如
subList(from,to).clear()会删原列表那段)。 - 误区:subList 后可以随意改原列表。 不能——对原列表做结构性修改(add/remove)后,再用 subList 会抛 ConcurrentModificationException(视图检测到 modCount 变了失效)。
- 误区:subList 会复制数据所以占内存。 不复制——它只是视图(持有原列表引用+偏移),零拷贝、省内存、创建快;要复制得自己 new ArrayList。
- 追问:为什么修改 subList 会影响原列表? subList 返回的是原列表的视图,内部持有原列表引用和 from/to 偏移,对视图的操作被翻译成对原列表对应位置的操作,所以共享底层数据、双向联动。
- 追问:subList 后对原列表 add 元素,再操作 subList 为什么抛 CME? subList 视图记录了原列表创建时的 modCount,原列表 add(结构性修改)使 modCount 变化,subList 操作时检测到 modCount 不一致就抛 ConcurrentModificationException(视图失效、fail-fast)。
- 追问:怎么从 subList 得到一个独立的列表? 用
new ArrayList<>(list.subList(from, to))把视图内容复制到新列表——新列表和原列表脱钩,改任一个不影响另一个,也不会因原列表结构修改而 CME。
八、加强记忆
list.subList(from, to) 返回的不是独立新列表,而是原列表的「视图(view)」——内部持有原列表引用和 from/to 偏移,对视图的操作被翻译成对原列表对应位置的操作,所以共享底层数据、双向联动:改 subList 影响原列表(set/add/remove/clear 都作用到原列表,如 subList(from,to).clear() 删原列表那段)、改原列表也影响 subList。最高频的坑:subList 后对原列表做「结构性修改」(add/remove 改变大小)会让 subList 失效、再操作抛 ConcurrentModificationException(视图记录了原列表 modCount,原列表结构变了检测到不一致就 fail-fast)。要独立子列表(不联动、不 CME)得 new ArrayList<>(list.subList(from, to)) 复制一份。subList 视图的有用场景是「零拷贝操作原列表的一段」——subList(from,to).clear() 高效删段、对一段排序/反转、只读查看(省内存)。它和 Arrays.asList(数组视图)、unmodifiableList(只读视图)、map.keySet()(Map 视图)都是「视图」——不拥有数据、和源数据联动,看到「返回集合」要问「是副本还是视图」(副本独立、视图联动)。一句话「subList 是视图不是副本、和原列表双向联动、原列表结构性修改后 subList 抛 CME、要独立用 new ArrayList 复制、clear 删段是有用技巧」。