《开发实战》10 | 集合类:坑满地的List列表操作

使用 Arrays.asList 把数据转换为 List 的三个坑
int[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
log.info("list:{} size:{} class:{}", list, list.size(), list.get(0).getClass());
输出: [[I@7716f4],1,class [I

这个List 包含的其实是一个 int 数组,整个 List 的元素个数是 1,元素类型是整数数组。并不是我们期望的

其原因是,只能是把 int 装箱为 Integer,不可能把 int 数组装箱为 Integer 数组。Arrays.asList 方法传入的是一个泛型 T 类型可变参数,最终 int 数组整体作为了一个对象成为了泛型类型 T

public static <T> List<T> asList(T... a) {
  return new ArrayList<>(a);
}

直接遍历这样的 List 必然会出现 Bug
修复方式有两种:
如果使用 Java8 以上版本可以使用 Arrays.stream 方法来转换,否则可以把 int 数组声明为包装类型 Integer 数组.

int[] arr1 = {1, 2, 3};
List list1 = Arrays.stream(arr1).boxed().collect(Collectors.toList());

Integer[] arr2 = {1, 2, 3};
List list2 = Arrays.asList(arr2);

这样就可以得到我们期待的值
案例

String[] arr = {"1", "2", "3"};
List list = Arrays.asList(arr);
arr[1] = "4";
System.out.println(list); // [1, 4, 3]
list.add("5"); // 抛异常

Arrays.asList 返回的 List 不支持增删操作
Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类 ArrayList。ArrayList 内部类继承自AbstractList 类,并没有覆写父类的 add 方法,而父类中 add 方法的实现,就是抛出UnsupportedOperationException

对原始数组的修改会影响到我们获得的那个 List
看一下 ArrayList 的实现,可以发现 ArrayList 其实是直接使用了原始的数组,很容易因为共享了数组,相互修改产生Bug。

修复的方式:重新 new 一个 ArrayList 初始化 Arrays.asList 返回的 List 即可

String[] arr = {"1", "2", "3"};
List list = new ArrayList(Arrays.asList(arr));
arr[1] = "4";
list.add("5");
log.info("arr:{} list:{}", Arrays.toString(arr), list);

使用 List.subList 进行切片操作居然会导致 OOM?

业务开发时常常要对 List 做切片处理,即取出其中部分元素构成一个新的 List,我们通常会想到使用 List.subList 方法。但,和 Arrays.asList 的问题类似,List.subList 返回的子List 不是一个普通的 ArrayList。这个子 List 可以认为是原始 List 的视图,会和原始 List 相互影响。如果不注意,很可能会因此产生 OOM 问题。
定义一个名为 data 的静态 List 来存放 Integer 的 List,也就是说 data 的成员本身是包含了多个数字的 List。循环 1000 次,每次都从一个具有 10 万个 Integer 的List 中,使用 subList 方法获得一个只包含一个数字的子 List,并把这个子 List 加入 data变量.

private static List<List<Integer>> data = new ArrayList<>();
private static void oom() {
  for (int i = 0; i < 1000; i++) {
    List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());
    data.add(rawList.subList(0, 1));
  }
}

很快就出现oom了,原因是,循环中的 1000 个具有 10 万个元素的 List 始终得不到回收,因为它始终被 subList 方法返回的 List 强引用。

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.add(0);
subList.forEach(System.out::println);
输出:
[2, 3, 4]
[1, 2, 4, 5, 6, 7, 8, 9, 10]
java.util.ConcurrentModificationException
	at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
	....
  1. 原始 List 中数字 3 被删除了,说明删除子 List 中的元素影响到了原始 List;
  2. 尝试为原始 List 增加数字 0 之后再遍历子 List,会出现ConcurrentModificationException。

查看ArrayList的源码

  1. ArrayList 维护了一个叫作 modCount 的字段,表示集合结构性修改的次数。所谓结构性修改,指的是影响 List 大小的修改,所以 add 操作必然会改变 modCount 的值。
  2. 分析第 980 到 983 行的 subList 方法可以看到,获得的 List 其实是内部类 SubList,并不是普通的 ArrayList,在初始化的时候传入了 this。
  3. 分析第 1001 到 1008 行代码可以发现,这个 SubList 中的 parent 字段就是原始的List。SubList 初始化的时候,并没有把原始 List 中的元素复制到独立的变量中保存。我们可以认为 SubList 是原始 List 的视图,并不是独立的 List。双方对元素的修改会相互影响,而且 SubList 强引用了原始的 List,所以大量保存这样的 SubList 会导致 OOM。
  4. 分析第 java.util.ArrayList.SubList#listIterator 和 java.util.ArrayList.SubList#checkForComodification 代码可以发现,遍历 SubList 的时候会先获得迭代器,比较原始ArrayList modCount 的值和 SubList 当前 modCount 的值。获得了 SubList 后,我们为原始 List 新增了一个元素修改了其 modCount,所以判等失败抛出ConcurrentModificationException 异常。

所以修复方式有2种方式:

  1. 不直接使用 subList 方法返回的 SubList,而是重新使用 new ArrayList,在构造方法传入 SubList,来构建一个独立的 ArrayList;
  2. 对于 Java 8 使用 Stream 的 skip 和 limit API 来跳过流中的元素,以及限制流中元素的个数,同样可以达到 SubList 切片的目的。
//方式一:
List<Integer> subList = new ArrayList<>(list.subList(1, 4));
//方式二:
List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());

一定要让合适的数据结构做合适的事情

第一个误区是,使用数据结构不考虑平衡时间和空间
搜索 ArrayList 的时间复杂度是 O(n),而 HashMap 的 get 操作的时间复杂度是 O(1)。所以,要对大 List 进行单值搜索的话,可以考虑使用 HashMap,其中 Key 是要搜索的值,Value 是原始对象,会比使用 ArrayList 有非常明显的性能优势。
如果要对大 ArrayList 进行去重操作,也不建议使用 contains 方法,而是可以考虑使用HashSet 进行去重
使用 HashMap 是否会牺牲空间呢?
使用 ObjectSizeCalculator 工具打印 ArrayList 和 HashMap 的内存占用,可以看到 ArrayList 占用内存 21M,而 HashMap 占用的内存达到了 72M,是 List 的三倍多
所以需要平衡的艺术,空间换时间,还是时间换空间。
第二个误区是,过于迷信教科书的大 O 时间复杂度
数据结构中要实现一个列表,有基于连续存储的数组和基于指针串联的链表两种方式。在Java 中,有代表性的实现是 ArrayList 和 LinkedList,前者背后的数据结构是数组,后者则是(双向)链表
实验证明,就是是大数据量的插入操作,也是ArrayList好
在各种常用场景下,LinkedList 几乎都不能在性能上胜出 ArrayList。
讽刺的是,LinkedList 的作者约书亚 · 布洛克(Josh Bloch),在其推特上回复别人时说,虽然 LinkedList 是我写的但我从来不用,有谁会真的用吗?

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值