Java-集合类-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)));
        }
    }
  • 直接将所需的元素复制到一个新的列表中,而不是使用子列表,这样可以切断新列表与原大列表之间的引用关系
  • 18
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小孔靠得住

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值