Arrays.asList和subList使用需要注意的大坑
Arrays.asList() 是 Java 中一个常用的方法,它 用于将数组转换为列表(List)。这个方法非常方便,但也有一些 需要注意的“大坑”。
一、Java-集合类-Arrays.asList()
大坑
1、不可修改列表大小&&原始数组与列表共享数据
由Arrays.asList()
的源码可知,转换后的列表是固定大小
的,这意味着你不能增加或删除元素,但可以修改现有元素(如果它们是可变对象的话)。转换后得到的列表与原始数组共享相同的底层数组
。这意味着对列表的修改会影响到原始数组,反之亦然。
- 不可修改列表元素的类型
Integer[] array = {1, 2, 3};
List<Integer> list = Arrays.asList(array);
// 下面这行代码会抛出 UnsupportedOperationException
list.add(4); // 错误:无法添加元素
- 原始数组与列表共享数据
Integer[] array = {1, 2, 3};
List<Integer> list = Arrays.asList(array);
list.set(0, 99); // 修改列表的第一个元素
System.out.println(array[0]); // 输出 99,因为数组也被修改了
2、对于基本类型数组的使用限制
如果你尝试用基本类型数组(如 int[])调用 Arrays.asList(),结果并不是你期望的列表,而是一个包含单个元素(即整个数组本身)的列表。
int[] primitiveArray = {1, 2, 3};
List<int[]> list = Arrays.asList(primitiveArray);
// list 现在是包含一个元素的列表,这个元素是原始数组 primitiveArray
两个错误案例
wrong1
private static void wrong1() {
int[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
log.info("list:{} size:{} class:{}", list, list.size(), list.get(0).getClass());
}
运行结果:
按道理输出结果list.size()也应该等于3才对,实际输出了个很奇怪的结果,我们从上诉“大坑2”可知,这里用基本类型数组调用Arrays.asList()了,所以得到的并不是期望的结果
正确方式:
private static void right1() {
int[] arr1 = {1, 2, 3};
List list1 = Arrays.stream(arr1).boxed().collect(Collectors.toList());
log.info("list:{} size:{} class:{}", list1, list1.size(), list1.get(0).getClass());
Integer[] arr2 = {1, 2, 3};
List list2 = Arrays.asList(arr2);
log.info("list:{} size:{} class:{}", list2, list2.size(), list2.get(0).getClass());
}
- Arrays.stream(arr1)将数组转换为流(Stream),boxed()将流中的int值包装为Integer对象,collect(Collectors.toList())将流收集到一个新的List列表中
- 最好直接用包装类:Integer[] arr2
运行结果:
wrong2
private static void wrong2() {
String[] arr = {"1", "2", "3"};
List list = Arrays.asList(arr);
arr[1] = "4";
try {
list.add("5");
} catch (Exception ex) {
ex.printStackTrace();
}
log.info("arr:{} list:{}", Arrays.toString(arr), list);
}
运行结果:
直接报错。由“大坑1”可知,此处直接往转换后的list中,添加数据,所以直接添加失败
正确方式:
private static void right2() {
String[] arr = {"1", "2", "3"};
List list = new ArrayList(Arrays.asList(arr));
arr[1] = "4";
try {
list.add("5");
} catch (Exception ex) {
ex.printStackTrace();
}
log.info("arr:{} list:{}", Arrays.toString(arr), list);
}
运行结果:
- 直接重新new一个ArrayList对象,开辟新的空间即可
二、Java-集合类-list.subList
subList方法是Java中List接口的一个成员方法,用于从现有的列表中获取一个子列表视图,这个视图包含了原列表中指定范围的元素。具体来说,该方法的签名如下:
public List<E> subList(int fromIndex, int toIndex)
-
参数说明:
fromIndex
: 子列表的起始位置(包含)。这个索引必须是非负的,并且小于toIndex。toIndex
: 子列表的结束位置(不包含)。这个索引必须是非负的,并且不大于列表的大小。
-
返回值: 返回一个新的列表视图,包含原列表中从fromIndex(包括)到toIndex(不包括)位置的元素。
注意事项
- 共享数据: subList返回的列表是一个视图,它与原列表共享相同的底层数据结构。这意味着对子列表的修改会影响到原列表,反之亦然。
- 不可变性: 虽然子列表是可修改的(可以添加、删除元素等),但这些修改会反映到原列表中,因此原列表的结构并不是不变的。
- 异常处理:
- 如果试图通过子列表修改原列表大小(如在子列表的边界外添加或删除元素),可能会导致UnsupportedOperationException。
- 在迭代子列表时,如果原列表被其他线程修改,可能会抛出ConcurrentModificationException。
- 类型转换: 不能将subList的结果强制转换为ArrayList,因为实际返回的是一个内部类,如RandomAccessSubList,这样的转换会导致ClassCastException。
- 性能影响: 对于支持快速随机访问(如ArrayList)的列表,subList性能较好;但对于不支持快速随机访问的列表(如LinkedList),频繁的子列表操作可能会影响性能。
- 边界检查: 在调用subList时,如果索引超出范围,会抛出IndexOutOfBoundsException。
不支持序列化: subList返回的对象通常不支持序列化,尝试序列化可能会失败。
大坑
1、ConcurrentModificationException
1)错误方式
private static void wrong() {
List<Integer> list = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList());
List<Integer> subList = list.subList(1, 4);
System.out.println(subList);
subList.remove(1);
System.out.println(list);
//list和subList维护的是同一个列表对象,并且该列表对象维护了一个modCount,
//当modCount和expectedModCount不相等时,抛出ConcurrentModificationException异常
list.add(0); //会修改同一个列表对象的modCount,导致抛出ConcurrentModificationException异常
try {
subList.forEach(System.out::println);
} catch (Exception ex) {
ex.printStackTrace();
}
}
这段代码将会引发 java.util.ConcurrentModificationException 异常。这是因为在这个场景中,当对list进行修改(通过list.add(0))后,再尝试遍历其子列表subList,根据Java的fail-fast机制,集合在迭代过程中检测到modCount被修改,则会抛出此异常以防止并发修改导致的数据不一致性问题。
2)正确方式1:
private static void right1() {
List<Integer> list = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList());
List<Integer> subList = new ArrayList<>(list.subList(1, 4));
System.out.println(subList);
subList.remove(1);
System.out.println(list);
list.add(0);
subList.forEach(System.out::println);
}
通过创建子列表的新实例new ArrayList<>(list.subList(1, 4))避免了这一问题,因为这样操作后,对原列表的修改不会影响到子列表的迭代,从而避免了异常抛出
3)正确方式2:
private static void right2() {
List<Integer> list = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList());
List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());
System.out.println(subList);
subList.remove(1);
System.out.println(list);
list.add(0);
subList.forEach(System.out::println);
}
- 避免了并发修改异常:通过使用stream().skip(1).limit(3).collect(Collectors.toList())来创建子列表,实际上是创建了原列表的一个独立副本。这意味着对subList的修改或对list的修改互不影响,因此在调用list.add(0)之后,遍历subList不会抛出ConcurrentModificationException异常,这一点与right1()函数的解决方案相似。
- 实现方式不同:虽然两者都有效避免了并发修改异常,但right2()采用了Stream API中的skip()和limit()方法来切片列表,这种方式更加灵活且表达意图更清晰,它不需要像right1()那样显式地复制子列表。
2、OOM
1)错误方式
private static void oom() {
for (int i = 0; i < 100000; i++) {
List<Integer> rawList = IntStream.rangeClosed(1, 1000000).boxed().collect(Collectors.toList());
data.add(rawList.subList(0, 1));
}
}
结果:堆空间不够了,直接宕机了
大量创建大列表
: 在每次循环中,都通过IntStream.rangeClosed(1, 1000000).boxed().collect(Collectors.toList())创建一个含有一百万个元素的Integer列表。这个操作本身非常消耗内存。但是也不至于直接报OOM,这里只是为更好看到效果子列表引用问题
: 虽然每次循环只取这个大列表的前一个元素作为子列表rawList.subList(0, 1)加入到另一个列表data中,但是subList方法返回的是原列表的一个视图(view),这意味着它并不真正复制数据,而是保留了对原列表的引用。因此,即使只是添加了每个大列表的一个小片段到data中,但由于这些子列表仍然引用着它们对应的大型原始列表,导致大量内存无法被垃圾回收,最终引发内存溢出。
2)正确方式
private static void oomfix() {
for (int i = 0; i < 100000; i++) {
List<Integer> rawList = IntStream.rangeClosed(1, 1000000).boxed().collect(Collectors.toList());
data.add(new ArrayList<>(rawList.subList(0, 1)));
}
}
- 直接将所需的元素复制到一个新的列表中,而不是使用子列表,这样可以切断新列表与原大列表之间的引用关系