我们来详细又通俗地聊一聊ArrayList的subList()方法以及它的“坑”和注意事项。
你可以把它想象成:这不是复制,而是开了一个“共享视图”或“实时窗口”。
一、核心本质:它是个“视图”,不是“副本”
当你调用list.subList(fromIndex, toIndex)时,它返回的不是一个全新的、独立的ArrayList对象。而是基于原有列表(我们叫它“父列表”)直接开辟的一个视图(View)。
通俗比喻:
你的父列表list就像一栋长长的公寓楼。subList(2, 5)就像是给你一个双筒望远镜,让你只能看到第3间到第5间公寓(索引从0开始)。你没有创造新的公寓,你只是限定了观察的范围。任何通过这个望远镜看到的事情(修改公寓里的东西),都会真实地发生在原公寓楼上。反之,如果有人直接去改动原公寓楼你这个视野范围内的房间,你通过望远镜也能立刻看到变化。
二、四大核心注意事项(坑点)
1. 【最关键的坑】对子列表的修改会直接影响原列表(父列表)
这是由于子列表的所有操作最终都“委托”给了原列表。
示例代码:
ArrayList<String> fatherList = new ArrayList<>();
fatherList.add(“A”);
fatherList.add(“B”);
fatherList.add(“C”);
fatherList.add(“D”);
fatherList.add(“E”);
// 获取一个子列表视图,包含 “B”, “C”, “D”
List<String> subList = fatherList.subList(1, 4);
System.out.println(“原列表: ” + fatherList); // [A, B, C, D, E]
System.out.println(“子列表: ” + subList); // [B, C, D]
// 修改子列表
subList.set(0, “B-改了”); // 将子列表的第0个元素(即原列表的”B”)修改
System.out.println(“修改子列表后”);
System.out.println(“原列表: ” + fatherList); // [A, B-改了, C, D, E] <- 原列表也被改了!
System.out.println(“子列表: ” + subList); // [B-改了, C, D]
// 在子列表上添加元素
subList.add(“F”);
System.out.println(“子列表添加元素后”);
System.out.println(“原列表: ” + fatherList); // [A, B-改了, C, D, F, E] <- 注意”F”被插入了!
System.out.println(“子列表: ” + subList); // [B-改了, C, D, F]
为什么会这样?
由于subList并没有自己存储数据,它只是记录了原列表的偏移量(从哪个索引开始,到哪个索引结束)。当你操作subList时,它内部会换算成对原列表fatherList相应位置的操作。
2. 【结构性修改的坑】原列表被修改后,再使用子列表会立刻抛出异常
这是一个超级常见的运行时错误。如果你在获取子列表之后,对原列表进行了结构性的修改(Structural Modification),然后再去操作之前获得的那个子列表,就会立刻抛出
ConcurrentModificationException异常。
结构性修改:指任何改变列表大小的操作,如 add(), remove(), clear()等。只修改元素值(如 set())不算。
非结构性修改:指不改变列表大小的操作,如set()修改元素值。
示例代码:
ArrayList<String> fatherList = new ArrayList<>(Arrays.asList(“A”, “B”, “C”, “D”, “E”));
List<String> subList = fatherList.subList(1, 4); // 获取视图
// 然后,对原列表进行结构性修改
fatherList.add(“F”); // 改变了原列表的结构和大小
// fatherList.remove(0); // 或者删除也行
// fatherList.clear(); // 或者清空更致命
// 此时,之前获取的 subList 已经失效了!
// 接下来任何对 subList 的操作(甚至只是打印它)都会抛出异常
System.out.println(subList.size()); // 抛出
ConcurrentModificationException
// subList.get(0); // 也会抛出异常
通俗解释:
你的望远镜(子列表)是根据最初公寓楼的结构和长度调整好的。突然有一天,公寓楼本身被加盖了一层或者拆掉了一层(原列表结构改变),你之前调好的望远镜的焦距和角度就全都错乱了,再通过它看东西,当然会出问题(抛出异常)。
3. 【转换的坑】不能直接强转成 ArrayList
subList()方法返回的是List接口的一个内部实现类(SubList),不是ArrayList对象。如果你尝试强制转换,会抛出 ClassCastException。
错误示范:
ArrayList<String> fatherList = new ArrayList<>(Arrays.asList(“A”, “B”, “C”, “D”));
// 这行代码会编译通过,但运行时会抛出 ClassCastException
ArrayList<String> subList = (ArrayList<String>) fatherList.subList(1, 3);
如果你需要一个独立的、真正的ArrayList 该怎么办?
创建一个新的ArrayList,把子列表的内容作为参数传进去。
正确做法:
ArrayList<String> fatherList = new ArrayList<>(Arrays.asList(“A”, “B”, “C”, “D”));
// 创建一个全新的、独立的 ArrayList
ArrayList<String> realNewList = new ArrayList<>(fatherList.subList(1, 3));
这样,你对 realNewList 的任何操作都不会影响 fatherList,反之亦然。
4. 【内存泄漏的潜在风险】长期持有子列表会导致原列表无法被回收
这是一个相对隐蔽但重大的问题。由于子列表持有对原列表的强引用,如果你有一个超级庞大的原列表hugeList,然后你只从中取了一个很小的子列表tinySublist并长期持有(列如放在一个静态变量里)。
即使hugeList本身你已经不再需要了,但由于tinySublist这个“望远镜”还在看着它,垃圾回收器就无法回收整个庞大的 hugeList,导致内存泄漏。
如何避免? 如果只是需要一小部分数据长期使用,应该像上一条说的一样,创建一个新的独立列表,然后让原列表和子列表都尽快失效。
三、一句话终极提议
把subList()当作一个临时、共享的实时视图来使用。如果你需要的是一个独立的、不受原列表影响的副本,请毫不犹豫地使用new ArrayList<>(list.subList(from, to))来创建一个新的列表。















暂无评论内容