List 的10个坑,保证你一定遇到过!
在我们实际开发中,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)
强引用,得不到回收
解决方式:
在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的关系。
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()
方法主要逻辑
通过遍历找到那个节点的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
的优势
最好再进行实际的性能测试评估,来得到最合适的数据结构。
CopyOnWriteArrayList内存占用过多
CopyOnWrite
,顾名思义就是写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。
CopyOnWriteArrayList
的add()
方法
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个坑,保证你一定遇到过!相关推荐
- 聊聊并发编程的10个坑
前言 对于从事后端开发的同学来说,并发编程肯定再熟悉不过了. 说实话,在java中并发编程是一大难点,至少我是这么认为的.不光理解起来比较费劲,使用起来更容易踩坑. 不信,让继续往下面看. 今天重点跟 ...
- python index false_整理了 Python新手 最容易犯错的 10个坑!
原标题:整理了 Python新手 最容易犯错的 10个坑! 相比于其他语言,Python 的语法比较简单易学,但一旦不注意细节,刚入门的新手就会掉进语法错误的坑里.今天给大家讲几个平常比较容易掉的&q ...
- 手游测试常见10个坑及填坑建议
随着手游市场的竞争越来越激烈,测试发布已经成为了几乎所有游戏的必经之路.捷克AppAgent公司的Peter Fodor指出了测试发布最容易陷进去的10个坑,并且根据他们常见到或者亲身经历过的错误给出 ...
- 线程池使用的10个坑(二)
目录 日常开发中,为了更好管理线程资源,减少创建线程和销毁线程的资源损耗,我们会使用线程池来执行一些异步任务.但是线程池使用不当,就可能会引发生产事故.今天田螺哥跟大家聊聊线程池的10个坑.大家看完肯 ...
- 细数 List 的10个坑,保证你一定遇到过
前言 今天我们主要来说一说List操作在实际使用中有哪些坑,以及面对这些坑的时候我们要怎么解决. 1. Arrays.asList转换基本类型数组的坑 在实际的业务开发中,我们通常会进行数组转List ...
- 细数 List 的10个坑,保证你一定遇到过!
前言 今天我们主要来说一说List操作在实际使用中有哪些坑,以及面对这些坑的时候我们要怎么解决. Arrays.asList转换基本类型数组的坑 在实际的业务开发中,我们通常会进行数组转List的操作 ...
- 避开日常Kubernetes最常见的10个坑
导读 使用 Kubernetes,大家都会遇到哪些错误?本文分享了作者多年来使用 Kubernetes 最常见的 10 个错误. 使用 Kubernetes,大家都会遇到哪些错误?本文分享了作者多年来 ...
- 达内培训python靠谱吗_注意躲避!Python初学者较容易掉进去的10个坑!
Python已经成为较受欢迎的程序设计语言之一.自从2004年以后,python的使用率呈线性增长.2011年1月,它被TIOBE编程语言排行榜评为2010年度语言. 相比于其他语言,Python的语 ...
- python入门必备10个坑_适合 Python 初学者的一些技巧和坑
前言 前几天董大 @董伟明 的爱派森上线了,还发了一套 PPT 和视频,PPT 共有75页干货,讲了很多进阶的用法,也包括很多坑,非常适合刚入门的同学,免得再踩到. 这里我提取部分非常规的,也就是大部 ...
最新文章
- TensorFlow、PyTorch之后,“国产”AI框架还有没有机会?
- SAP MM 采购附加费计入物料成本?
- 盘点丨2017年亚洲新晋18家独角兽公司
- 2017年全球AI芯片公司大盘点
- 15、mybatis一对多关联查询 collection定义关联集合封装规则及懒加载
- iOS开发网络篇—网络请求(HTTP协议)小结(转)
- Spring Boot JPA中java 8 的应用
- ios-UIButton-常用方法
- ubuntu报错E: 无法获得锁 /var/cache/apt/archives/lock - open (11: 资源暂时不可用)E: 无法对目录 /var/cache/apt/archives/
- android 切换字体崩溃,androidx - 在Android 10 / Android Q上使用捆绑的ttf字体时崩溃 - 堆栈内存溢出...
- 全国院线总票房破 50 亿!影院复工后,哪些电影最受欢迎?
- DAMS峰会丨从数仓到数据中台,从数据资源到资产,京东、携程、快手等是怎么建设和演进的?...
- HTML实现图文混排效果
- flutter (笔记) 动画学习
- 代理记账的基本流程是什么
- 共享锁与排他锁的关系
- go-map数据类型详解
- 嵌入式100题(81):波特率是什么,为什么双方波特率要相同,高低波特率有什么区别;...
- 【LeGO-LOAM论文阅读(二)--特征提取(一)】
- 网页前端培训(CSS)