JAVA面试|ArrayList的subList()返回集合有什么注意事项

我们来详细又通俗地聊一聊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))来创建一个新的列表。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容