List的10个坑

一、Arrays.asList转换基本类型数组的坑

1.1 问题展示

在实际的业务开发中,我们通常会进行数组转List的操作,通常我们会使用Arrays.asList来进行转换,但是在转换基本类型的数组的时候,却出现转换的结果和我们想象的不一致。

public class Test {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        List list = Arrays.asList(arr);
        System.out.println(list.size());
        System.out.println(list);
    }
}

打印结果

1
[[I@2b193f2d]

实际上,我们想要转成的List应该是有三个对象而现在只有一个

asList源码

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

可以观察到 asList方法 接收的是一个泛型T类型的参数,T继承Object对象,所以通过断点我们可以看到把 int数组 整体作为一个对象,返回了一个List

1.2 解决方案
1.2.1 方案1

Java8以上,利用Arrays.stream(arr).boxed()将装箱为Integer数组

List collect = Arrays.stream(arr).boxed().collect(Collectors.toList()); 
System.out.println(collect.size());
System.out.println(collect.get(0).getClass());
// 3
// class java.lang.Integer
1.2.2 方案2

声明数组的时候,声明类型改为包装类型

Integer[] integerArr = {1, 2, 3};
List integerList = Arrays.asList(integerArr);
System.out.println(integerList.size()); 
System.out.println(integerList.get(0).getClass());
// 3
// class java.lang.Integer

二、Arrays.asList返回的List不支持增删操作

我们将数组对象转成List数据结构之后,竟然不能进行增删操作了

private static void asListAdd(){
    String[] arr = {"1", "2", "3"};
    List strings = new ArrayList<>(Arrays.asList(arr));
    arr[2] = "4";
    System.out.println(strings.toString());
    Iterator iterator = strings.iterator();
    while (iterator.hasNext()){
        if ("4".equals(iterator.next())){
            iterator.remove();
        }
    }
    strings.forEach(val ->{
        strings.remove("4");
        strings.add("3");
    });


    System.out.println(Arrays.asList(arr).toString());
}

[1, 2, 4]
Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.remove(AbstractList.java:161) at java.util.AbstractList$Itr.remove(AbstractList.java:374) at java.util.AbstractCollection.remove(AbstractCollection.java:293) at JavaBase.List.AsListTest.lambda$asListAdd$0(AsListTest.java:47) at java.util.Arrays$ArrayList.forEach(Arrays.java:3880) at JavaBase.List.AsListTest.asListAdd(AsListTest.java:46) at JavaBase.List.AsListTest.main(AsListTest.java:20)

初始化一个字符串数组,将字符串数组转换为 List,在遍历List的时候进行移除和新增的操作抛出异常信息UnsupportedOperationException。根据异常信息java.lang.UnsupportedOperationException,我们看到他是从AbstractList里面出来的,让我们进入源码一看究竟我们在什么时候调用到了这个AbstractList 呢?其实Arrays.asList(arr) 返回的 ArrayList 不是 java.util.ArrayList,而是Arrays的内部类

private static class ArrayList extends AbstractList
        implements RandomAccess, java.io.Serializable{
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;
    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }

    @Override
    public E get(int index) {}

    @Override
    public E set(int index, E element) {...}

...
}
public abstract class AbstractList extends AbstractCollection implements List {
    public boolean add(E e) {
        add(size(), e);
        return true;
    }
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

    public E remove(int index) {
        throw new UnsupportedOperationException();
    }

}

他是没有实现AbstractList中的add()remove()方法,这里就很清晰了为什么不支持新增和删除,因为根本没有实现。

三、对原始数组的修改会影响到我们获得的那个List

3.1 问题展示

一不小心修改了父List,却影响到了子List,在业务代码中,这会导致产生的数据发生变化,严重的话会造成影响较大的生产问题。第二个坑的源码中,完成字符串数组转换为List之后,我们将字符串数组的第三个对象的值修改为4,但是很奇怪在打印List的时候,发现List也发生了变化。

3.2 解决方案

重新new一个新的ArrayList来装返回的List

List strings = new ArrayList<>(Arrays.asList(arr));  

四、java.util.ArrayList如果不正确操作也不支持增删操作

在第二个坑的时候,我们说到了Arrays.asList返回的List不支持增删操作,是因为他的自己实现了一个内部类ArrayList,这个内部类继承了 AbstractList 没有实现add()remove()方法导致操作失败。但是第三个坑的时候,我们利用java.util.ArrayList包装了返回的List,进行增删操作还是会失败,那是为什么呢?

在foreach中操作增删,因为 modCount 会被修改,与第一步保存的数组修改次数不一致,抛出异常ConcurrentModificationException

在正确操作是什么?我总结了四种方式

1

五、ArrayList中的subList强转ArrayList导致异常

5.1 问题展示

阿里《Java开发手册》上提过:[强制] ArrayList的sublist结果不可強转成ArrayList,否则会抛出ClassCastException异常,即java.util.RandomAccesSubList cannot be cast to java. util.ArrayList。

说明: subList 返回的是ArrayList 的内部类SubList, 并不是ArrayList ,而是ArrayList的一个视图,対于SubList子列表的所有操作最终会反映到原列表上。

其实SubList是一个继承AbstractList的内部类,在SubList的构建函数中的将List中的部分属性直接赋予给自己。SubList没有创建一个新的List,而是直接引用了原来的List(this.parent = parent),指定了元素的范围。所以subList方法不能直接转成ArrayList,他只是ArrayList的内部类,没有其他的关系。因为是引用的关系,所以在这里也需要特别的注意,如果对原来的List进行修改,会对产生的subList结果产生影响。

对subList产生的List做出结构型修改,操作会反应到原来的List上,ongChange也添加到了names中如果修改原来的List则会抛出异常ConcurrentModificationException。

5.2 解决方案

在操作SubList的时候,new一个新的ArrayList来接收创建subList结果的拷贝

List strings = new ArrayList(names.subList(0, 1));

六、ArrayList中的subList切片造成OOM

6.1 问题展示

在业务开发中的时候,他们经常通过subList来获取所需要的那部分数据。在上面的例子中,我们知道了subList所产生的List,其实是对原来List对象的引用,这个产生的List只是原来List对象的视图,也就是说虽然值切片获取了一小段数据,但是原来的List对象却得不到回收,这个原来的List对象可能是一个很大的对象。

为了方便我们测试,将vm调整一下 -Xms20m -Xmx40m

private static void subListOomTest(){  

    IntStream.range(0, 1000).forEach(i ->{  
        List collect = IntStream.range(0, 100000).boxed().collect(Collectors.toList());  
            data.add(collect.subList(0, 1));  

        });  

    }}  

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space  

出现OOM的原因,循环1000次创建了1000个具有10万个元素的List,因为始终被collect.subList(0, 1)强引用,得不到回收

6.2 解决方案
6.2.1 方案1

在subList方法返回SubList,重新使用new ArrayList,来构建一个独立的ArrayList

List list = new ArrayList<>(collect.subList(0, 1));  
6.2.2 方案2

利用Java8的Stream中的skip和limit来达到切片的目的

List list = collect.stream().skip(0).limit(1).collect(Collectors.toList());

在这里我们可以看到,只要用一个新的容器来装结果,就可以切断与原始List的关系。

七、LinkedList与ArrayList插入速度

学习数据结构的时候,我们就已经得出了结论:

  • 对于数组,随机元素访问的时间复杂度是0(1), 元素插入操作是O(n);
  • 对于链表,随机元素访问的时间复杂度是O(n), 元素插入操作是0(1);

其实该结论不一定对,看以下测试代码(测试环境为JDK1.8):

public class Test {
    public static void main(String[] args) {
        test02();
    }

    private static void test01() {
        TimeInterval timer = DateUtil.timer();
        List arrayList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            arrayList.add(i);
        }
        //arrayList:6毫秒
        System.out.println("arrayList:" + timer.intervalPretty());


        TimeInterval timer2 = DateUtil.timer();
        List linkedList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            linkedList.add(i);
        }
        //linkedList:1毫秒
        System.out.println("linkedList:" + timer2.intervalPretty());
    }

    private static void test02() {
        TimeInterval timer = DateUtil.timer();
        List arrayList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            arrayList.add(i, i);
        }
        //arrayList:6毫秒
        System.out.println("arrayList:" + timer.intervalPretty());


        TimeInterval timer2 = DateUtil.timer();
        List linkedList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            linkedList.add(i, i);
        }
        //linkedList:2毫秒
        System.out.println("linkedList:" + timer2.intervalPretty());
    }
}

测试结果LinkedList插入确实比ArrayList快,但ArrayList插入却不是O(n),我们可以看源码。

ArrayList.add()源码:

   public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

ArrayList记录了size,ArrayList插入的时候是插在数组尾部的,所以并没有O(n),而是O(1)。

我们再来看LinkedList.add()源码:

   public boolean add(E e) {
        linkLast(e);
        return true;
    }

LinkedList也是直接插在尾部的,所以确实是O(1)。

LinkedList和ArrayList添加元素时都是在尾部插入,时间复杂度都是O(1),那为什么测试结果确是LinkedList比ArrayList快?那是因为ArrayList插的时候,到了一定数量会扩容,扩容需要消耗一些时间,所以ArrayList插入才比LinkedList慢。

八、CopyOnWriteArrayList内存占用过多

CopyOnWrite,顾名思义就是写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。

CopyOnWriteArrayList 内部维护了一个数组,成员变量 array 就指向这个内部数组,所有的读操作都是基于新的array对象进行的。因为上了独占锁,所以如果多个线程调用add()方法只有一个线程会获得到该锁,其他线程被阻塞,知道锁被释放, 由于加了锁,所以整个操作的过程是原子性操作。CopyOnWriteArrayList 会将 新的array复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将复制的结果指向这个新的数组。

由于每次写入的时候都会对数组对象进行复制,复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,所以当列表中的元素比较少的时候,这对内存和 GC 并没有多大影响,但是当列表保存了大量元素的时候,对 CopyOnWriteArrayList 每一次修改,都会重新创建一个大对象,并且原来的大对象也需要回收,这都可能会触发 GC,如果超过老年代的大小则容易触发Full GC,引起应用程序长时间停顿。

九、CopyOnWriteArrayList是弱一致性的

调用iterator方法获取迭代器返回一个COWIterator对象。COWIterator的构造器里主要是 保存了当前的list对象的内容和遍历list时数据的下标。snapshot是list的快照信息,因为CopyOnWriteArrayList的读写策略中都会使用getArray()来获取一个快照信息,生成一个新的数组。所以在使用该迭代器元素时,其他线程对该lsit操作是不可见的,因为操作的是两个不同的数组所以造成弱一致性。

private static void CopyOnWriteArrayListTest(){
    CopyOnWriteArrayList list = new CopyOnWriteArrayList();
    list.add("test1");
    list.add("test2");
    list.add("test3");
    list.add("test4");

    Thread thread = new Thread(() -> {
        System.out.println(">>>> start");
        list.add(1, "replaceTest");
        list.remove(2);
    });

    // 在启动线程前获取迭代器
    Iterator iterator = list.iterator();

    thread.start();

    try {
        // 等待线程执行完毕
        thread.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    while (iterator.hasNext()){
        System.out.println(iterator.next());
    }
}

>>>> start
test1
test2
test3
test4

上面的demo中在启动线程前获取到了原来list的迭代器,在之后启动新建一个线程,在线程里面修改了第一个元素的值,移除了第二个元素,在执行完子线程之后,遍历了迭代器的元素,发现子线程里面操作的一个都没有生效,这里提现了迭代器弱一致性。

十、CopyOnWriteArrayList的迭代器不支持增删改

private static void CopyOnWriteArrayListTest(){
    CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
    list.add("test1");
    list.add("test2");
    list.add("test3");
    list.add("test4");

    Iterator iterator = list.iterator();

    while (iterator.hasNext()){
        if ("test1".equals(iterator.next())){
            iterator.remove();
        }
    }

    System.out.println(list.toString());
}

Exception in thread "main" java.lang.UnsupportedOperationException
 at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)

CopyOnWriteArrayList迭代器是只读的,不支持增删操作,CopyOnWriteArrayList迭代器中的remove()add()方法,没有支持增删而是直接抛出了异常。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。

转载自:细数 List 的10个坑,保证你一定遇到过

你可能感兴趣的:(List的10个坑)