05 | ArrayList还是LinkedList?使用不当性能差千倍
05 | ArrayList还是LinkedList?使用不当性能差千倍
讲述:李良
时长15:24大小14.10M
初识 List 接口
ArrayList 是如何实现的?
1.ArrayList 实现类
2.ArrayList 属性
3.ArrayList 构造函数
4.ArrayList 新增元素
5.ArrayList 删除元素
6.ArrayList 遍历元素
LinkedList 是如何实现的?
1.LinkedList 实现类
2.LinkedList 属性
3.LinkedList 新增元素
4.LinkedList 删除元素
5.LinkedList 遍历元素
总结
思考题
赞 11
提建议
精选留言(76)
- 陆离置顶2019-05-30对于arraylist和linkedlist的性能以前一直都是人云亦云,大家都说是这样那就这样吧,我也从来没有自己去验证过,没想过因操作位置的不同差异还挺大。 当然这里面有一个前提,那就是arraylist的初始大小要足够大。 思考题是第一个是正确的,第二个虽然用的是foreach语法糖,遍历的时候用的也是迭代器遍历,但是在remove操作时使用的是原始数组list的remove,而不是迭代器的remove。 这样就会造成modCound != exceptedModeCount,进而抛出异常。展开
作者回复: 陆离同学一直保持非常稳定的发挥,答案非常准确!
共 2 条评论98 - Rain置顶2019-05-30老师,为什么第二种就会抛出`ConcurrentModificationException`异常呢,我觉得第一种迭代器会抛这个异常啊
作者回复: for(:)循环[这里指的不是for(;;)]是一个语法糖,这里会被解释为迭代器,在使用迭代器遍历时,ArrayList内部创建了一个内部迭代器iterator,在使用next()方法来取下一个元素时,会使用ArrayList里保存的一个用来记录List修改次数的变量modCount,与iterator保存了一个expectedModCount来表示期望的修改次数进行比较,如果不相等则会抛出异常; 而在在foreach循环中调用list中的remove()方法,会走到fastRemove()方法,该方法不是iterator中的方法,而是ArrayList中的方法,在该方法只做了modCount++,而没有同步到expectedModCount。 当再次遍历时,会先调用内部类iteator中的hasNext(),再调用next(),在调用next()方法时,会对modCount和expectedModCount进行比较,此时两者不一致,就抛出了ConcurrentModificationException异常。 所以关键是用ArrayList的remove还是iterator中的remove。
70 - Loubobooo2019-06-01这一道我会。如果有看过阿里java规约就知道,在集合中进行remove操作时,不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。 <!-- 规约第七条 -->
作者回复: 👍
共 2 条评论33 - 脱缰的野马__2020-03-15老师您好,在我的认知里面,之所以数组遍历比链表要快,应该还有一个底层的原因,就是源于数组的实现是在内存当中是一块连续的内存空间,而链表所有元素可能分布在内存的不同位置,对于数组这种数据结构来说对CPU读是非常友好的,不管是CPU从内存读数据读到高速缓存还是线程从磁盘读数据到内存时,都不只是读取需要的那部分数据,而是读取相关联的某一块地址数据,这样的话对于在遍历数组的时候,在一定程度上提高了CPU高速缓存的命中率,减少了CPU访问内存的次数从而提高了效率,这是我结合计算机相关原理的角度考虑的一点。展开
作者回复: 赞,这是计算机底层访问数组和链表的实现原理,数组因为存储地址是连续的,所以在每次访问某个元素的时候,会将某一块连续地址的数据读取到CPU缓存中,这也是数组查询快于链表的关键。
29 - 皮皮2019-05-30第一种写法正确,第二种会报错,原因是上述两种写法都有用到list内部迭代器Iterator,而在迭代器内部有一个属性是exceptedmodcount,每次调用next和remove方法时会检查该值和list内部的modcount是否一致,不一致会报异常。问题中的第二种写法remove(e),会在每次调用时modcount++,虽然迭代器的remove方法也会调用list的这个remove(e)方法,但每次调用后还有一个exceptedmodcount=modcount操作,所以后续调用next时判断就不会报异常了。展开
作者回复: 关键在用谁的remove方法。
17 - TerryGoForIt2019-05-31老师您好,我比较好奇的是为什么 ArrayList 不像 HashMap 一样在扩容时需要一个负载因子呢?
作者回复: HashMap有负载因子是既要考虑数组太短,因哈希冲突导致链表过长而导致查询性能下降,也考虑了数组过长,新增数据时性能下降。这个负载因子是综合了数组和链表两者的长度,不能太大也不能太小。而ArrayList不需要这种考虑。
15 - JasonZ2019-06-09linkedlist使用iterator比普通for循环效率高,是由于遍历次数少,这是为什么?有什么文档可以参考么?
作者回复: 因为for循环需要遍历链表,每循环一次就需要遍历一次指定节点前的数据,源码如下: // 获取双向链表中指定位置的节点 private Entry<E> entry(int index) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+size); Entry<E> e = header; // 获取index处的节点。 // 若index < 双向链表长度的1/2,则从前先后查找; // 否则,从后向前查找。 if (index < (size >> 1)) { for (int i = 0; i <= index; i++) e = e.next; } else { for (int i = size; i > index; i--) e = e.previous; } return e; } 而iterator在第一次拿到一个数据后,之后的循环中会使用Iterator中的next()方法采用的是顺序访问。
共 8 条评论11 - csyangchsh2019-05-30测试代码不严谨,建议使用JMH。
作者回复: 厉害了,感谢建议。这里很多同学没有了解过JMH测试框架,所以没有使用。
10 - 建国2019-06-10老师,您好,linkList查找元素通过分前后半段,每次查找都要遍历半个list,怎么就知道元素是出于前半段还是后半段的呢?
作者回复: 这个是随机的,因为分配的内存地址不是连续的。
共 4 条评论6 - ABC2019-05-31谢谢老师明白了,如果第二种写法换成for(;;)就会直接调用ArrayList的remove()方法就不会报错了。 在第二种写法里面用foreach相当于是使用ArrayList内部的Itr类进行遍历,但删除数据又是用的ArrayList里面的remove()方法。从而导致状态不一致,引发报错。6
- 多襄丸2019-12-24modCount属于ArrayList expectedModCount属于Iterator 增强for循环 本质是iterator遍历 iterator循环 iterator遍历 增强for循环 调用list.remove() 不会修改到iterator的expectedModCount, 从而导致 迭代器的expectedModCount != ArrayList的modCound; 迭代器会抛出 concurrentModifiedException 而iterator遍历 的时候 用iterator. remove(); modCount 会被同步到expectedModCount中去,ArrayList的modCount == Iterator的exceptedModCount,所以不会抛出异常。 老师对其他同学的评论以及我的理解就是这样。展开
作者回复: 赞
4 - L.2019-08-06老师,随机访问到底是什么意思?怎么个随机法?谢谢~
作者回复: 这里指的是不需要通过遍历寻址,可以通过index直接访问到内存地址。
共 2 条评论4 - 业余草2019-05-31请问:List<A> list = new ArrayList<>(); for(int i=0;i++;i<1000){ A a = new A(); list.add(a); } 和 和 这个 List<A> list = new ArrayList<>(); A a; for(int i=0;i++;i<1000){ a = new A(); list.add(a); } 效率上有差别吗?不说new ArrayList<>(); 初始化问题。单纯说创建对象这一块。谢谢!展开
作者回复: 没啥区别的,可以实际操作试试
3 - 老杨同志2019-05-30写法一正确,写法二会快速失败3
- 麦兜布熊2020-03-31老师,什么场景会用到linkedlist呢?我好像只见过Arraylist的代码呢
作者回复: 在做一些业务功能开发时,我们平常用的最多的是ArrayList,因为ArrayList就能满足我们的业务需求,单次填充列表以及单次全部读取列表。 到经常有删除/插入操作的情况下适合使用LinkedList,例如我们要写一个类似LRU算法的缓存功能,就可以用到LinkedList。
2 - 张三丰2019-11-06first/last 方式可以在初始化 LinkedList 的时候节省 new 一个 Entry; 来回看老师的专栏,这是第三遍了,每次都会有新的理解,同时也有新的疑问产生,比如上边的那句话今天一直没有想明白。。。。麻烦老师详细解答一下。共 1 条评论2
- Aaron_涛2019-07-17arrayList,for循环访问快是因为内存连续,可以整个缓存行读取进cpu缓存中,遍历下个的时候无需去内存中获取。并不是实现什么随机获取接口
作者回复: 是的,是因为连续内存。在代码中,程序是不知道底层开辟的内存情况,所以需要一个类似序列化的接口标志,这个接口仅仅是一个标志,并不是实现。
共 2 条评论2 - gavin2019-06-11老师好,怎么确定操作集合是从头部、中间、还是尾部操作的呢?
作者回复: arraylist的add方法默认是从尾部操作,delete方法就是根据自己指定的位置来删除;linkedlist的add方法也是默认从尾部插入元素,delete方法也是根据指定的元素来删除。
2 - DebugDog2019-05-30写法一正确。 虽然都是调用了remove方法,但是两个remove方法是不同的。 写法二是有可能会报ConcurrentModificationException异常。 所以在ArrayList遍历删除元素时使用iterator方式或者普通的for循环。
作者回复: 对的,使用普通循环也需要注意。
2 - mickle2019-05-30第二种不行吧,会报并发修改异常的2