环球热点!细数List的10个坑

简介: 今天我们主要来说一说List操作在实际使用中有哪些坑,以及面对这些坑的时候我们要怎么解决。

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

在实际的业务开发中,我们通常会进行数组转List的操作,通常我们会使用Arrays.asList来进行转换


(资料图)

但是在转换基本类型的数组的时候,却出现转换的结果和我们想象的不一致。

上代码

int[] arr = {1, 2, 3}; List list = Arrays.asList(arr); System.out.println(list.size()); // 1

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

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

可以观察到 asList方法 接收的是一个泛型T类型的参数,T继承Object对象

所以通过断点我们可以看到把 int数组 整体作为一个对象,返回了一个 List

那我们该如何解决呢?

方案一: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

方案二:声明数组的时候,声明类型改为包装类型

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

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

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

private static void asListAdd(){    String[] arr = {"1", "2", "3"};    Liststrings = new ArrayList<>(Arrays.asList(arr));    arr[2] = "4";    System.out.println(strings.toString());    Iteratoriterator = 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 ArrayListextends AbstractListimplements 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 AbstractListextends AbstractCollectionimplements 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() 方法,这里就很清晰了为什么不支持新增和删除,因为根本没有实现。

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

一不小心修改了父List,却影响到了子List,在业务代码中,这会导致产生的数据发生变化,严重的话会造成影响较大的生产问题。

第二个坑的源码中,完成字符串数组转换为List之后,

我们将字符串数组的第三个对象的值修改为4,但是很奇怪在打印List的时候,发现List也发生了变化。

public static ListasList(T... a) {    return new ArrayList<>(a);}ArrayList(E[] array) {    a = Objects.requireNonNull(array);}

asList中创建了 ArrayList,但是他直接引用了原本的数组对象

所以只要原本的数组对象一发生变化,List也跟着变化

所以在使用到引用的时候,我们需要特别的注意。

解决方案:

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

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

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

在第二个坑的时候,我们说到了 Arrays.asList 返回的 List 不支持增删操作,

是因为他的自己实现了一个内部类 ArrayList,这个内部类继承了 AbstractList 没有实现 add() 和 remove() 方法导致操作失败。

但是第三个坑的时候,我们利用 java.util.ArrayList 包装了返回的 List,进行增删操作还是会失败,那是为什么呢?

删除方法逻辑:

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

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

5. ArrayList中的 subList 强转 ArrayList 导致异常

阿里《Java开发手册》上提过

[强制] ArrayList的sublist结果不可強转成ArrayList,否则会抛出ClassCastException

异常,即java.util.RandomAccesSubList cannot be cast to java. util.ArrayList.

说明: subList 返回的是ArrayList 的内部类SubList, 并不是ArrayList ,而是

ArrayList的一个视图,対于SubList子列表的所有操作最终会反映到原列表上。

private static void subListTest(){    Listnames = new ArrayList() {{    add("one");    add("two");    add("three");}};    ArrayList strings = (ArrayList) names.subList(0, 1);    System.out.println(strings.toString());}Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

我猜问题是有八九就是出现在subList这个方法上了

private class SubList extends AbstractListimplements RandomAccess {    private final AbstractListparent;    private final int parentOffset;    private final int offset;    int size;    SubList(AbstractListparent,    int offset, int fromIndex, int toIndex) {    this.parent = parent;    this.parentOffset = fromIndex;    this.offset = offset + fromIndex;    this.size = toIndex - fromIndex;    this.modCount = ArrayList.this.modCount;}}

其实 SubList 是一个继承 AbstractList 的内部类,在 SubList 的构建函数中的将 List 中的部分属性直接赋予给自己

SubList 没有创建一个新的 List,而是直接引用了原来的 List(this.parent = parent),指定了元素的范围

所以 subList 方法不能直接转成 ArrayList,他只是ArrayList的内部类,没有其他的关系

因为是引用的关系,所以在这里也需要特别的注意,如果对原来的List进行修改,会对产生的 subList结果产生影响。

Listnames = new ArrayList() {{    add("one");    add("two");    add("three");}};List strings = names.subList(0, 1);strings.add(0, "ongChange");System.out.println(strings.toString());System.out.println(names.toString());[ongChange, one][ongChange, one, two, three]

对subList产生的List做出结构型修改,操作会反应到原来的List上,ongChange也添加到了names中

如果修改原来的List则会抛出异常ConcurrentModificationException

Listnames = new ArrayList() {{    add("one");    add("two");    add("three");}};List strings = names.subList(0, 1);names.add("four");System.out.println(strings.toString());System.out.println(names.toString());Exception in thread "main" java.util.ConcurrentModificationException

原因:

subList的时候记录this.modCount为3

原来的List插入了一个新元素,导致this.modCount不第一次保存的不一致则抛出异常

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

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

6. ArrayList中的subList切片造成OOM

在业务开发中的时候,他们经常通过subList来获取所需要的那部分数据

在上面的例子中,我们知道了subList所产生的List,其实是对原来List对象的引用

这个产生的List只是原来List对象的视图,也就是说虽然值切片获取了一小段数据,但是原来的List对象却得不到回收,这个原来的List对象可能是一个很大的对象

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

private static void subListOomTest(){IntStream.range(0, 1000).forEach(i ->{Listcollect = 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)强引用,得不到回收

解决方式:

在subList方法返回SubList,重新使用new ArrayList,来构建一个独立的ArrayList
List list = new ArrayList<>(collect.subList(0, 1));
利用Java8的Stream中的skip和limit来达到切片的目的
List list = collect.stream().skip(0).limit(1).collect(Collectors.toList());

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

7. LinkedList的插入速度不一定比ArrayList快

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

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

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

元素插入对于链表来说应该是他的优势

但是他就一定比数组快? 我们执行插入1000w次的操作

private static void test(){    StopWatch stopWatch = new StopWatch();    int elementCount = 100000;    stopWatch.start("ArrayList add");    ListarrayList = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(ArrayList::new));    // ArrayList插入数据    IntStream.rangeClosed(0, elementCount).forEach(i ->arrayList.add(ThreadLocalRandom.current().nextInt(elementCount), 1));    stopWatch.stop();    stopWatch.start("linkedList add");    ListlinkedList = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(LinkedList::new));    // ArrayList插入数据    IntStream.rangeClosed(0, elementCount).forEach(i ->linkedList.add(ThreadLocalRandom.current().nextInt(elementCount), 1));    stopWatch.stop();    System.out.println(stopWatch.prettyPrint());}StopWatch "": running time = 44507882 ns---------------------------------------------ns         %     Task name---------------------------------------------043836412  098%  elementCount 100 ArrayList add000671470  002%  elementCount 100 linkedList addStopWatch "": running time = 196325261 ns---------------------------------------------ns         %     Task name---------------------------------------------053848980  027%  elementCount 10000 ArrayList add142476281  073%  elementCount 10000 linkedList addStopWatch "": running time = 26384216979 ns---------------------------------------------ns         %     Task name---------------------------------------------978501580  004%  elementCount 100000 ArrayList add25405715399  096%  elementCount 100000 linkedList add

看到在执行插入1万、10完次操作的时候,LinkedList的插入操作时间是 ArrayList的两倍以上

那问题主要就是出现在linkedList的 add()方法上

public void add(int index, E element) {    checkPositionIndex(index);    if (index == size)        linkLast(element);    else        linkBefore(element, node(index));}    /*** Returns the (non-null) Node at the specified element index.    */Nodenode(int index) {    // assert isElementIndex(index);    if(index < (size >>1)) {        Nodex = first;        for (int i = 0; i < index; i++)            x = x.next;        return x;    } else {        Nodex = last;        for (int i = size - 1; i >index; i--)            x = x.prev;        return x;    }}

linkedList的 add()方法主要逻辑

通过遍历找到那个节点的Node 执行插入操作

ArrayList的 add()方法

public void add(int index, E element) {    rangeCheckForAdd(index);    ensureCapacityInternal(size + 1);  // Increments modCount!!    System.arraycopy(elementData, index, elementData, index + 1,                     size - index);    elementData[index] = element;    size++;}
计算最小容量 最小容量大于数组对象,则进行扩容 进行数组复制,根据插入的index将数组向后移动一位 最后在空位上插入新值

根据试验的测试,我们得出了在实际的随机插入中,LinkedList并没有比ArrayList的速度快

所以在实际的使用中,如果涉及到头尾对象的操作,可以使用LinkedList数据结构来进行增删的操作,发挥LinkedList的优势

最好再进行实际的性能测试评估,来得到最合适的数据结构。

点击查看原文,获取更多福利!

https://developer.aliyun.com/article/1095706?utm_content=g_1000366144

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

关键词: 抛出异常 数据结构 一个新的

推荐DIY文章
主机存在磨损或划痕风险 PICO4便携包宣布召回
穿越湖海!特斯拉Cybertruck电动皮卡可以当“船”用
vivoXFold+折叠旗舰开售 配备蔡司全焦段旗舰四摄
飞凡R7正式上市 全系标配换电架构
中兴Axon30S开售 拥有黑色蓝色两款配色
荣耀MagicBookV14 2022正式开售 搭载TOF传感器
it