你碰到过吗】如果面试官问你ArrayList和LinkedList有什么区别?

引言

  • ArrayList,LinkedList,Vector,CopyOnWriteArrayList 底层实现原理和四个集合的区别是什么?
  • 为什么工作中会常用ArrayList和CopyOnWriteArrayList?
  • 如果面试官问你ArrayList和LinkedList有什么区别?

上图:

image

基础介绍

ArrayList : 基于数组实现的非线程安全的集合。查询元素快,插入,删除中间元素慢。 LinkedList : 基于链表实现的非线程安全的集合。查询元素慢,插入,删除中间元素快。 Vector : 基于数组实现的线程安全的集合。线程同步(方法被synchronized修饰),性能比ArrayList差。 CopyOnWriteArrayList: 基于数组实现的线程安全的写时复制集合。线程安全(ReentrantLock加锁),性能比Vector高,适合读多写少的场景。

ArrayList 和 LinkedList 读写快慢的本质

ArrayList : 查询数据快,是因为数组可以通过下标直接找到元素。 写数据慢有两个原因:一是数组复制过程需要时间,二是扩容需要实例化新数组也需要时间。 LinkedList : 查询数据慢,是因为链表需要遍历每个元素直到找到为止。 写数据快有一个原因:除了实例化对象需要时间外,只需要修改指针即可完成添加和删除元素。

:这里的快和慢是相对的。并不是LinkedList的插入和删除就一定比ArrayList快。明白其快慢的本质:ArrayList快在定位,慢在数组复制。LinkedList慢在定位,快在指针修改。

ArrayList

ArrayList 是基于动态数组实现的非线程安全的集合。当底层数组满的情况下还在继续添加的元素时,ArrayList则会执行扩容机制扩大其数组长度。ArrayList查询速度非常快,使得它在实际开发中被广泛使用。美中不足的是插入和删除元素较慢,同时它并不是线程安全的。

 
  1. // 查询元素

  2. public E get(int index) {

  3. rangeCheck(index); // 检查是否越界

  4. return elementData(index);

  5. }

  6. // 顺序添加元素

  7. public boolean add(E e) {

  8. ensureCapacityInternal(size + 1); // 扩容机制

  9. elementData[size++] = e;

  10. return true;

  11. }

  12. // 从数组中间添加元素

  13. public void add(int index, E element) {

  14. rangeCheckForAdd(index); // 数组下标越界检查

  15. ensureCapacityInternal(size + 1); // 扩容机制

  16. System.arraycopy(elementData, index, elementData, index + 1, size - index); // 复制数组

  17. elementData[index] = element; // 替换元素

  18. size++;

  19. }

  20. // 从数组中删除元素

  21. private void fastRemove(int index) {

  22. modCount++;

  23. int numMoved = size - index - 1;

  24. if (numMoved > 0)

  25. System.arraycopy(elementData, index+1, elementData, index, numMoved);

  26. elementData[--size] = null; // clear to let GC do its work

  27. }

从源码中可以得知,

ArrayList在执行查询操作时: 第一步:先判断下标是否越界。 第二步:然后在直接通过下标从数组中返回元素。

ArrayList在执行顺序添加操作时: 第一步:通过扩容机制判断原数组是否还有空间,若没有则重新实例化一个空间更大的新数组,把旧数组的数据拷贝到新数组中。 第二步:在新数组的最后一位元素添加值。

ArrayList在执行中间插入操作时: 第一步:先判断下标是否越界。 第二步:扩容。 第三步:若插入的下标为i,则通过复制数组的方式将i后面的所有元素,往后移一位。 第四步:新数据替换下标为i的旧元素。 删除也是一样:只是数组往前移了一位,最后一个元素设置为null,等待JVM垃圾回收。

从上面的源码分析,我们可以得到一个结论和一个疑问。 结论是:ArrayList快在下标定位,慢在数组复制。 疑问是:能否将每次扩容的长度设置大点,减少扩容的次数,从而提高效率?其实每次扩容的长度大小是很有讲究的。若扩容的长度太大,会造成大量的闲置空间;若扩容的长度太小,会造成频发的扩容(数组复制),效率更低。

LinkedList

LinkedList 是基于双向链表实现的非线程安全的集合,它是一个链表结构,不能像数组一样随机访问,必须是每个元素依次遍历直到找到元素为止。其结构的特殊性导致它查询数据慢。

 
  1. // 查询元素

  2. public E get(int index) {

  3. checkElementIndex(index); // 检查是否越界

  4. return node(index).item;

  5. }

  6. Node<E> node(int index) {

  7. if (index < (size >> 1)) { // 类似二分法

  8. Node<E> x = first;

  9. for (int i = 0; i < index; i++)

  10. x = x.next;

  11. return x;

  12. } else {

  13. Node<E> x = last;

  14. for (int i = size - 1; i > index; i--)

  15. x = x.prev;

  16. return x;

  17. }

  18. }

  19. // 插入元素

  20. public void add(int index, E element) {

  21. checkPositionIndex(index); // 检查是否越界

  22. if (index == size) // 在链表末尾添加

  23. linkLast(element);

  24. else // 在链表中间添加

  25. linkBefore(element, node(index));

  26. }

  27. void linkBefore(E e, Node<E> succ) {

  28. final Node<E> pred = succ.prev;

  29. final Node<E> newNode = new Node<>(pred, e, succ);

  30. succ.prev = newNode;

  31. if (pred == null)

  32. first = newNode;

  33. else

  34. pred.next = newNode;

  35. size++;

  36. modCount++;

  37. }

从源码中可以得知,

LinkedList在执行查询操作时: 第一步:先判断元素是靠近头部,还是靠近尾部。 第二步:若靠近头部,则从头部开始依次查询判断。和ArrayList的elementData(index)相比当然是慢了很多。

LinkedList在插入元素的思路: 第一步:判断插入元素的位置是链表的尾部,还是中间。 第二步:若在链表尾部添加元素,直接将尾节点的下一个指针指向新增节点。 第三步:若在链表中间添加元素,先判断插入的位置是否为首节点,是则将首节点的上一个指针指向新增节点。否则先获取当前节点的上一个节点(简称A),并将A节点的下一个指针指向新增节点,然后新增节点的下一个指针指向当前节点。

Vector

Vector 的数据结构和使用方法与ArrayList差不多。最大的不同就是Vector是线程安全的。从下面的源码可以看出,几乎所有的对数据操作的方法都被synchronized关键字修饰。synchronized是线程同步的,当一个线程已经获得Vector对象的锁时,其他线程必须等待直到该锁被释放。从这里就可以得知Vector的性能要比ArrayList低。

若想要一个高性能,又是线程安全的ArrayList,可以使用Collections.synchronizedList(list);方法或者使用CopyOnWriteArrayList集合

 
  1. public synchronized E get(int index) {

  2. if (index >= elementCount)

  3. throw new ArrayIndexOutOfBoundsException(index);

  4.  
  5. return elementData(index);

  6. }

  7. public synchronized boolean add(E e) {

  8. modCount++;

  9. ensureCapacityHelper(elementCount + 1);

  10. elementData[elementCount++] = e;

  11. return true;

  12. }

  13. public synchronized boolean removeElement(Object obj) {

  14. modCount++;

  15. int i = indexOf(obj);

  16. if (i >= 0) {

  17. removeElementAt(i);

  18. return true;

  19. }

  20. return false;

  21. }

CopyOnWriteArrayList

在这里我们先简单了解一下CopyOnWrite容器。它是一个写时复制的容器。当我们往一个容器添加元素的时候,不是直接往当前容器添加,而是先将当前容器进行copy一份,复制出一个新的容器,然后对新容器里面操作元素,最后将原容器的引用指向新的容器。所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。

应用场景:适合高并发的读操作(读多写少)。若写的操作非常多,会频繁复制容器,从而影响性能。

CopyOnWriteArrayList 写时复制的集合,在执行写操作(如:add,set,remove等)时,都会将原数组拷贝一份,然后在新数组上做修改操作。最后集合的引用指向新数组。

CopyOnWriteArrayList 和Vector都是线程安全的,不同的是:前者使用ReentrantLock类,后者使用synchronized关键字。ReentrantLock提供了更多的锁投票机制,在锁竞争的情况下能表现更佳的性能。就是它让JVM能更快的调度线程,才有更多的时间去执行线程。这就是为什么CopyOnWriteArrayList的性能在大并发量的情况下优于Vector的原因。

 
  1. private E get(Object[] a, int index) {

  2. return (E) a[index];

  3. }

  4. public boolean add(E e) {

  5. final ReentrantLock lock = this.lock;

  6. lock.lock();

  7. try {

  8. Object[] elements = getArray();

  9. int len = elements.length;

  10. Object[] newElements = Arrays.copyOf(elements, len + 1);

  11. newElements[len] = e;

  12. setArray(newElements);

  13. return true;

  14. } finally {

  15. lock.unlock();

  16. }

  17. }

  18. private boolean remove(Object o, Object[] snapshot, int index) {

  19. final ReentrantLock lock = this.lock;

  20. lock.lock();

  21. try {

  22. Object[] current = getArray();

  23. int len = current.length;

  24. ......

  25. Object[] newElements = new Object[len - 1];

  26. System.arraycopy(current, 0, newElements, 0, index);

  27. System.arraycopy(current, index + 1, newElements, index, len - index - 1);

  28. setArray(newElements);

  29. return true;

  30. } finally {

  31. lock.unlock();

  32. }

  33. }

更多Java源码、数据库、spring boot、内功心法等知识都是需要不断学习、积累、实践方能领会。因此我给大家推荐一个Java架构群:895244712,里面有分布式,微服务,性能优化等技术点底层原理的视频,也有众多想要提升的小伙伴讨论技术,欢迎大家加群一起交流学习。

总结

回到文章的标题,如果面试官问你ArrayList和LinkedList有什么区别?

ArrayList和LinkedList都不是线程安全的,小并发量的情况下可以使用Vector,若并发量很多,且读多写少可以考虑使用CopyOnWriteArrayList。 因为CopyOnWriteArrayList底层使用ReentrantLock锁,比使用synchronized关键字的Vector能更好的处理锁竞争的问题。

相信这个回答能够很好的帮你通过面试:p

转载于:https://my.oschina.net/u/3981166/blog/2250813

发布了51 篇原创文章 · 获赞 80 · 访问量 93万+

猜你喜欢

转载自blog.csdn.net/xiyang_1990/article/details/103888686