关于使用Arrays.asList和ArrayList.subList所踩的坑

在多数情况下使用asList或subList都是只读的,因此一般不会出现问题,但当有需要对list 数据修改时就会抛出异常。

1.asList

有时在对一个数据进行一些处理时,数组没有 List 一些快捷方法,此时就需要将数据转为 List 使用,因此使用不当而引发程序异常

1.1.正常使用

import java.util.Arrays;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        Integer[] numbers = new Integer[] {1, 2, 3, 4, 5};
        List<Integer> list = Arrays.asList(numbers);
        System.out.println("包含 3:" + list.contains(3));
        System.out.println("包含 6:" + list.contains(6));
    }
}

结果输出:

1324ed787bfc451ea7bfd5bf06e9c382.png

1.2.异常使用

当需要对 list 进行操作时会抛出异常

import java.util.Arrays;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        Integer[] numbers = new Integer[] {1, 2, 3, 4, 5};
        List<Integer> list = Arrays.asList(numbers);
        System.out.println("包含 3:" + list.contains(3));
        System.out.println("包含 6:" + list.contains(6));
        // 异常操作
        list.add(7);
    }
}

43c4be83623149609fdc192c9b966b6f.png

1.3.原因分析

通过查看Arrays 源码发现,asList 里面返回的 ArrayList 是位于 Arrays 里面的一个私有类

16fcaa3ab383405ebb5dbb975cab5174.png

这个类和java.util.ArrayList一样,也继承了AbstractList

a9eff0dab93e4ceda7f9c7c2822419b4.png

在 Arrays.ArrayList里面,它重写了 set、get、indexOf、contains等方法但没有重写 add 方法,这导致无法对 list 进行元素增加

804281965f144dce9cafad84c1f9ae47.png

因此,对于 asList 返回的 list 我们可以用来修改、获取等操作,但是无法增加元素和删除元素,因为 asList 引用的内部类 ArrayList 没有重写 add 和 remove 这些方法

1.4 衍生问题

对于这个问题进行深度研究发现,list 和numbers其实是公用的一个对象,对 list 操作其实就是对numbers操作,例如:将 list 第一个元素改为 9,其对应的 numbers第一个元素也将会变为 9

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        Integer[] numbers = new Integer[] {1, 2, 3, 4, 5};
        List<Integer> list = Arrays.asList(numbers);
        System.out.println("list包含 3:" + list.contains(3));
        System.out.println("list包含 6:" + list.contains(6));
        System.out.println("list包含 9:" + list.contains(9));
        list.set(0,9);
        System.out.println("list包含 9:" + list.contains(9));

        System.out.println("numbers[0]:" + numbers[0]);
    }
}

结果:

1b8b8c7b79ba43d9b6bd08e2f92cff74.png

2.subList

事故背景:原先有个业务需要对一批数据订阅,订阅数据是分段发送的,比如一次查询 1000 条,订阅的时候需要每次只发不超过 100 条,这个业务代码跑了很久一直没问题,直到一次功能迭代,新增了一个订阅,需要将这 1000 条数据和另外的几十条比较特殊的数据放在一起生成文件上传的 FTP,开发人员直接将需要加入的数据用 addAll 给加到原集合里面,然后再使用子集合时就发现程序抛出java.util.ConcurrentModificationException异常

1.1 原集合结构变更会引发异常

import java.util.ArrayList;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        List<Integer> sourceList = new ArrayList<Integer>() {{
            add(1);
            add(2);
            add(3);
            add(4);
            add(5);
        }};
        List<Integer> subList = sourceList.subList(0, 3);
        System.out.println("sourceList: " + sourceList);
        System.out.println("subList: " + subList);
        sourceList.add(6);
        System.out.println("subList: " + subList);
    }
}

结果:这种情况也只是修改原集合结构后使用了子集合才会抛异常

d7f00a64e8e8477c8aa12d9a764948ad.png

1.2子集合结构变更会影响原集合(注意元素位置)

import java.util.ArrayList;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        List<Integer> sourceList = new ArrayList<Integer>() {{
            add(1);
            add(2);
            add(3);
            add(4);
            add(5);
        }};
        List<Integer> subList = sourceList.subList(0, 3);
        System.out.println("sourceList: " + sourceList);
        System.out.println("subList: " + subList);
        subList.add(6);
        System.out.println("sourceList: " + sourceList);
        System.out.println("subList: " + subList);
    }
}

结果:在子集合末尾追加的元素,在原集合中的位置取决于子集合的toIndex

de07d4aa65474f84bf76a2ae5c932bba.png

1.3 原集合或子集合数据变更会相互影响

import java.util.ArrayList;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        List<Integer> sourceList = new ArrayList<Integer>() {{
            add(1);
            add(2);
            add(3);
            add(4);
            add(5);
        }};
        List<Integer> subList = sourceList.subList(0, 3);
        System.out.println("sourceList: " + sourceList);
        System.out.println("subList: " + subList);
        sourceList.set(0,2);
        System.out.println("sourceList: " + sourceList);
        System.out.println("subList: " + subList);
        subList.set(0,3);
        System.out.println("sourceList: " + sourceList);
        System.out.println("subList: " + subList);
    }
}

结果:修改了原集合元素子集合也会修改,修改了子集合原集合也会修改

eeef5e712d9d48bda983fb2514fed652.png

1.4 总结

  1. 修改原集合结构再使用子集合会抛异常
  2. 修改子集合结构原集合也会随之修改,追加元素的位置取决于toIndex
  3. 修改原集合的元素会影响子集合,修改子集合元素会影响原集合