在我们实际开发中,List应该是使用最多的集合之一,既然用的多,那我们就应该重视,免得出bug。

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

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

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() 方法,这里就很清晰了为什么不支持新增和删除,因为根本没有实现。

对原始数组的修改会影响到我们获得的那个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));

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

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

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

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

删除方法逻辑:

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

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

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));

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的关系。

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的两倍以上

那问题主要就是出现在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.  */
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;  }
}

linkedList的 add()方法主要逻辑

  1. 通过遍历找到那个节点的Node

  2. 执行插入操作

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++;
}
  1. 计算最小容量

  2. 最小容量大于数组对象,则进行扩容

  3. 进行数组复制,根据插入的index将数组向后移动一位

  4. 最后在空位上插入新值

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

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

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

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,引起应用程序长时间停顿。

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的迭代器,

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

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

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()方法,没有支持增删而是直接抛出了异常。

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

/*** 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. 聊聊并发编程的10个坑

    前言 对于从事后端开发的同学来说,并发编程肯定再熟悉不过了. 说实话,在java中并发编程是一大难点,至少我是这么认为的.不光理解起来比较费劲,使用起来更容易踩坑. 不信,让继续往下面看. 今天重点跟 ...

  2. python index false_整理了 Python新手 最容易犯错的 10个坑!

    原标题:整理了 Python新手 最容易犯错的 10个坑! 相比于其他语言,Python 的语法比较简单易学,但一旦不注意细节,刚入门的新手就会掉进语法错误的坑里.今天给大家讲几个平常比较容易掉的&q ...

  3. 手游测试常见10个坑及填坑建议

    随着手游市场的竞争越来越激烈,测试发布已经成为了几乎所有游戏的必经之路.捷克AppAgent公司的Peter Fodor指出了测试发布最容易陷进去的10个坑,并且根据他们常见到或者亲身经历过的错误给出 ...

  4. 线程池使用的10个坑(二)

    目录 日常开发中,为了更好管理线程资源,减少创建线程和销毁线程的资源损耗,我们会使用线程池来执行一些异步任务.但是线程池使用不当,就可能会引发生产事故.今天田螺哥跟大家聊聊线程池的10个坑.大家看完肯 ...

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

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

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

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

  7. 避开日常Kubernetes最常见的10个坑

    导读 使用 Kubernetes,大家都会遇到哪些错误?本文分享了作者多年来使用 Kubernetes 最常见的 10 个错误. 使用 Kubernetes,大家都会遇到哪些错误?本文分享了作者多年来 ...

  8. 达内培训python靠谱吗_注意躲避!Python初学者较容易掉进去的10个坑!

    Python已经成为较受欢迎的程序设计语言之一.自从2004年以后,python的使用率呈线性增长.2011年1月,它被TIOBE编程语言排行榜评为2010年度语言. 相比于其他语言,Python的语 ...

  9. python入门必备10个坑_适合 Python 初学者的一些技巧和坑

    前言 前几天董大 @董伟明 的爱派森上线了,还发了一套 PPT 和视频,PPT 共有75页干货,讲了很多进阶的用法,也包括很多坑,非常适合刚入门的同学,免得再踩到. 这里我提取部分非常规的,也就是大部 ...

最新文章

  1. TensorFlow、PyTorch之后,“国产”AI框架还有没有机会?
  2. SAP MM 采购附加费计入物料成本?
  3. 盘点丨2017年亚洲新晋18家独角兽公司
  4. 2017年全球AI芯片公司大盘点
  5. 15、mybatis一对多关联查询 collection定义关联集合封装规则及懒加载
  6. iOS开发网络篇—网络请求(HTTP协议)小结(转)
  7. Spring Boot JPA中java 8 的应用
  8. ios-UIButton-常用方法
  9. ubuntu报错E: 无法获得锁 /var/cache/apt/archives/lock - open (11: 资源暂时不可用)E: 无法对目录 /var/cache/apt/archives/
  10. android 切换字体崩溃,androidx - 在Android 10 / Android Q上使用捆绑的ttf字体时崩溃 - 堆栈内存溢出...
  11. 全国院线总票房破 50 亿!影院复工后,哪些电影最受欢迎?
  12. DAMS峰会丨从数仓到数据中台,从数据资源到资产,京东、携程、快手等是怎么建设和演进的?...
  13. HTML实现图文混排效果
  14. flutter (笔记) 动画学习
  15. 代理记账的基本流程是什么
  16. 共享锁与排他锁的关系
  17. go-map数据类型详解
  18. 嵌入式100题(81):波特率是什么,为什么双方波特率要相同,高低波特率有什么区别;...
  19. 【LeGO-LOAM论文阅读(二)--特征提取(一)】
  20. 网页前端培训(CSS)

热门文章

  1. 如何快速入门webvr的开发教程 - mxreality.js 简介
  2. OA办公系统如何实现多维智能提醒
  3. 什么是网络安全风险评估?
  4. 密度峰值聚类算法DPC(Density Peak Clustering)理论基础与python实现
  5. Win10安装Microsoft Store
  6. Unity Text字间距和行间距调整
  7. 从协方差分析看回归与方差分析的联系
  8. Anaconda3安装教程【详细图文教程】
  9. 应用商店成恶意APP滋生新温床 190款感染应用让你措不及防
  10. Tapd需求或BUG 关联gitlib提交