前言

今天我们主要来说一说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<int[]>

「那我们该如何解决呢?」

方案一: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"};List<String> strings = new ArrayList<>(Arrays.asList(arr));arr[2] = "4";System.out.println(strings.toString());Iterator<String> 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<E> extends AbstractList<E>implements RandomAccess, java.io.Serializable{private static final long serialVersionUID = -2764017481108945198L;private final E[] a;ArrayList(E[] array) {a = Objects.requireNonNull(array);}@Overridepublic E get(int index) {}@Overridepublic E set(int index, E element) {...}...
}
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {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 <T> List<T> asList(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(){  List<String> names = new ArrayList<String>() {{  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 AbstractList<E> implements RandomAccess {  private final AbstractList<E> parent;  private final int parentOffset;  private final int offset;  int size;  SubList(AbstractList<E> parent,  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结果产生影响。

List<String> names = new ArrayList<String>() {{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

List<String> names = new ArrayList<String>() {{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 ->{  List<Integer> 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)强引用,得不到回收

解决方式:

  1. 在subList方法返回SubList,重新使用new ArrayList,来构建一个独立的ArrayList
List list = new ArrayList<>(collect.subList(0, 1));
  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");List<Integer> arrayList = 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");List<Integer> linkedList = 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 add
000671470  002%  elementCount 100 linkedList addStopWatch '': running time = 196325261 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
053848980  027%  elementCount 10000 ArrayList add
142476281  073%  elementCount 10000 linkedList addStopWatch '': running time = 26384216979 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
978501580  004%  elementCount 100000 ArrayList add
25405715399  096%  elementCount 100000 linkedList add

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

那问题主要就是出现在linkedListadd()方法上

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.  */
Node<E> node(int index) {  // assert isElementIndex(index);  if(index < (size >> 1)) {  Node<E> x = first;  for (int i = 0; i < index; i++)  x = x.next;  return x;  } else {  Node<E> x = last;  for (int i = size - 1; i > index; i--)  x = x.prev;  return x;  }
}

linkedListadd()方法主要逻辑

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

ArrayListadd()方法

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++;
}
  1. 计算最小容量
  2. 最小容量大于数组对象,则进行扩容
  3. 进行数组复制,根据插入的index将数组向后移动一位
  4. 最后在空位上插入新值

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

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

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

8. CopyOnWriteArrayList内存占用过多

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

CopyOnWriteArrayListadd()方法

public boolean add(E e) {// 获取独占锁final ReentrantLock lock = this.lock;lock.lock();try {// 获取arrayObject[] elements = getArray();// 复制array到新数组,添加元素到新数组int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;// 替换数组setArray(newElements);return true;} finally {// 释放锁lock.unlock();}
}

CopyOnWriteArrayList 内部维护了一个数组,成员变量 array 就指向这个内部数组,所有的读操作都是基于新的array对象进行的。

因为上了独占锁,所以如果多个线程调用add()方法只有一个线程会获得到该锁,其他线程被阻塞,知道锁被释放, 由于加了锁,所以整个操作的过程是原子性操作

CopyOnWriteArrayList 会将 新的array复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将复制的结果指向这个新的数组。

由于每次写入的时候都会对数组对象进行复制,复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,所以当列表中的元素比较少的时候,这对内存和 GC 并没有多大影响,但是当列表保存了大量元素的时候,

CopyOnWriteArrayList 每一次修改,都会重新创建一个大对象,并且原来的大对象也需要回收,这都可能会触发 GC,如果超过老年代的大小则容易触发Full GC,引起应用程序长时间停顿。

9. CopyOnWriteArrayList是弱一致性的

public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);
}static final class COWIterator<E> implements ListIterator<E> {/** Snapshot of the array */private final Object[] snapshot;/** Index of element to be returned by subsequent call to next.  */private int cursor;private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;}public boolean hasNext() {return cursor < snapshot.length;}public boolean hasPrevious() {return cursor > 0;}@SuppressWarnings("unchecked")public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];}

调用iterator方法获取迭代器返回一个COWIterator对象

COWIterator的构造器里主要是 保存了当前的list对象的内容和遍历list时数据的下标。

snapshot是list的快照信息,因为CopyOnWriteArrayList的读写策略中都会使用getArray()来获取一个快照信息,生成一个新的数组。

所以在使用该迭代器元素时,其他线程对该lsit操作是不可见的,因为操作的是两个不同的数组所以造成弱一致性。

private static void CopyOnWriteArrayListTest(){CopyOnWriteArrayList<String> 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<String> 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的迭代器,

在之后启动新建一个线程,在线程里面修改了第一个元素的值,移除了第二个元素

在执行完子线程之后,遍历了迭代器的元素,发现子线程里面操作的一个都没有生效,这里提现了迭代器弱一致性。

10. CopyOnWriteArrayList的迭代器不支持增删改

private static void CopyOnWriteArrayListTest(){CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();list.add("test1");list.add("test2");list.add("test3");list.add("test4");Iterator<String> iterator = list.iterator();while (iterator.hasNext()){if ("test1".equals(iterator.next())){iterator.remove();}}System.out.println(list.toString());
}Exception in thread "main" java.lang.UnsupportedOperationExceptionat java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)

CopyOnWriteArrayList 迭代器是只读的,不支持增删操作

CopyOnWriteArrayList迭代器中的 remove()add()方法,没有支持增删而是直接抛出了异常。扩展:最全的java面试题库

因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。

/*** Not supported. Always throws UnsupportedOperationException.* @throws UnsupportedOperationException always; {@code remove}*         is not supported by this iterator.*/
public void remove() {throw new UnsupportedOperationException();
}/*** Not supported. Always throws UnsupportedOperationException.* @throws UnsupportedOperationException always; {@code set}*         is not supported by this iterator.*/
public void set(E e) {throw new UnsupportedOperationException();
}/*** Not supported. Always throws UnsupportedOperationException.* @throws UnsupportedOperationException always; {@code add}*         is not supported by this iterator.*/
public void add(E e) {throw new UnsupportedOperationException();
}

总结

由于篇幅的限制,我们只对一些在业务开发中常见的关键点进行梳理和介绍

在实际的工作中,我们不单单是要清除不同类型容器的特性,还要选择适合的容器才能做到事半功倍。

我们主要介绍了Arrays.asList转换过程中的一些坑,以及因为操作不当造成的OOM和异常,

到最后介绍了线程安全类CopyOnWriteArrayList的一些坑,让我们认识到在丰富的API下藏着许多的陷阱。

在使用的过程中,需要更加充分的考虑避免这些隐患的发生。

最后一张思维导图来回顾一下~

细数 List 的10个坑,保证你一定遇到过相关推荐

  1. 细数 List 的10个坑,保证你一定遇到过!

    前言 今天我们主要来说一说List操作在实际使用中有哪些坑,以及面对这些坑的时候我们要怎么解决. Arrays.asList转换基本类型数组的坑 在实际的业务开发中,我们通常会进行数组转List的操作 ...

  2. 细数Qt开发的各种坑(欢迎围观)

    1:Qt的版本多到你数都数不清,多到你开始怀疑人生.从4.6开始到5.8,从MSVC编译器到MINGW编译器,从32位到64位,从Windows到Linux到MAC.MSVC版本还必须安装对应的VS2 ...

  3. 做服务器_码迷SEO:细数那些做SEO巨坑的服务器们

    这是码迷SEO的第51篇原创这两天又有摩天楼的用户来找码迷看网站情况,说是网站坚持做好原创.做好配图.做好体验,是典型的三好学生网站.但是做了好长时间了,又没有蜘蛛.又没有排名.又没有流量,是典型的贫 ...

  4. 细数mui框架走过的坑

    一.2018.06.21mui.alert().mui.confirm()等弹窗系列 mui会根据ua判断,弹出原生对话框还是h5绘制的对话框,在基座中默认会弹出原生对话框,可以配置type属性,使得 ...

  5. php公众号支付后的微信通知,关于微信公众号支付细数我踩过的坑

    微信公众号支付流程 项目背景: 1.前后端分离,前端是jquery 获取后端接口 (php) 开发流程: 1.配置 进入微信公众平台,申请开通微信支付,不开通就别进行下一步了.开通后,进入左侧的微信支 ...

  6. 细数二十世纪最伟大的10大算法

    导读:作者July总结了一篇关于计算方法的文章< 细数二十世纪最伟大的10大算法 >. 一.1946 蒙特卡洛方法 [1946: John von Neumann, Stan Ulam, ...

  7. 细数sass安装中遇到的坑

    前言: 前两天打算清理电脑的时候,遇到了一点特殊的问题,打算重装一些东西,其中就有我一直用的顺手的SASS预编译工具. 但是在重装的时候,我发现我居然不会用了??? 靠,要不是我用了半年的Sass,我 ...

  8. 百密一疏,防不胜防,细数那些大型数据库建设过程中绕不开的坑

    构建大型数据库时,无论最开始的设计多么精妙,到后续操作的时候或多或少都会遇到一些问题,本文将来细数大型数据库中不可避免会遇到的问题. 原文标题:Feature Casualties of Large ...

  9. 细数流落民间的10大贵族CN域名

    自2003年中国国家顶级域名开放注册以来,已经有数百万的.CN域名被注册,顶峰时期注册量甚至达到千万以上,其中被企业终端收购并启用的.CN 域名更是不计其数,其中以中国移动10086.cn.中国电信1 ...

最新文章

  1. .f90文件批量转为dll文件_办公必备神器DropIt V8.5.1Portable文件整理分类工具
  2. Android 使用ORMLite 操作数据库
  3. JAVA基础知识(4)
  4. 扬尘监测系统_工地扬尘监测_工地扬尘监测解决方案
  5. JS入门之Date对象
  6. IPv6与IPv4比较
  7. php数据表创建命令代码,MySQL创建和删除数据表的命令及语法详解
  8. mysql怎么用迅雷下载_MySQL安装详细步骤(附迅雷下载链接)
  9. Q Learning学习笔记
  10. [转载]MIS专业排名
  11. 方正飞鸿智能信息平台产品白皮书(四)
  12. Redis6:第六章:(2)Redis6 新数据类型:HyperLogLog
  13. pytorch 中的 forward 的使用与解释
  14. github注册,使用方法
  15. 流利阅读 2019.1.9 No progress towards ending U.S. shutdown in Trump meeting with lawmakers
  16. 前端架构设计应该包含哪些东西?
  17. 利用记忆规律促进学生有效学习(记忆规律在教学中的运用)
  18. Equalize Them All(思维)
  19. 比赛得分java_(比赛)得分:_____
  20. 微机原理课程设计-模拟十字路口交通信号灯

热门文章

  1. HBuildX的下载与安装
  2. 名词性从句 - 主语从句
  3. Android Study 之 6分钟妥妥集成微信以及支付宝支付
  4. 行政责任与民事责任、刑事责任简单理解
  5. Ubuntu解压工具rar安装
  6. 花几个小时写的C++五子棋程序,就直接打败我了······
  7. 基于MFC的二维码打印工具总结
  8. 瑞士金融市场管理局发布ICO监管指南
  9. 德州监管机构针对海外ICO活动下发禁令
  10. Struggle to 搞懂统计学——点估计 区间估计 置信区间