- ArrayList,LinkedList,Vector,CopyOnWriteArrayList 底层实现原理和四个集合的区别是什么?
- 为何工做中会经常使用ArrayList和CopyOnWriteArrayList?
- 若是面试官问你ArrayList和LinkedList有什么区别?
上图:node
ArrayList : 基于数组实现的非线程安全的集合。查询元素快,插入,删除中间元素慢。 LinkedList : 基于链表实现的非线程安全的集合。查询元素慢,插入,删除中间元素快。 Vector : 基于数组实现的线程安全的集合。线程同步(方法被synchronized修饰),性能比ArrayList差。 CopyOnWriteArrayList: 基于数组实现的线程安全的写时复制集合。线程安全(ReentrantLock加锁),性能比Vector高,适合读多写少的场景。面试
ArrayList : 查询数据快,是由于数组能够经过下标直接找到元素。 写数据慢有两个缘由:一是数组复制过程须要时间,二是扩容须要实例化新数组也须要时间。 LinkedList : 查询数据慢,是由于链表须要遍历每一个元素直到找到为止。 写数据快有一个缘由:除了实例化对象须要时间外,只须要修改指针便可完成添加和删除元素。spring
注:这里的快和慢是相对的。并非LinkedList的插入和删除就必定比ArrayList快。明白其快慢的本质:ArrayList快在定位,慢在数组复制。LinkedList慢在定位,快在指针修改。数据库
ArrayList 是基于动态数组实现的非线程安全的集合。当底层数组满的状况下还在继续添加的元素时,ArrayList则会执行扩容机制扩大其数组长度。ArrayList查询速度很是快,使得它在实际开发中被普遍使用。美中不足的是插入和删除元素较慢,同时它并非线程安全的。数组
// 查询元素 public E get(int index) { rangeCheck(index); // 检查是否越界 return elementData(index); } // 顺序添加元素 public boolean add(E e) { ensureCapacityInternal(size + 1); // 扩容机制 elementData[size++] = e; return true; } // 从数组中间添加元素 public void add(int index, E element) { rangeCheckForAdd(index); // 数组下标越界检查 ensureCapacityInternal(size + 1); // 扩容机制 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 复制数组 elementData[index] = element; // 替换元素 size++; } // 从数组中删除元素 private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
从源码中能够得知,安全
ArrayList在执行查询操做时: 第一步:先判断下标是否越界。 第二步:而后在直接经过下标从数组中返回元素。性能优化
ArrayList在执行顺序添加操做时: 第一步:经过扩容机制判断原数组是否还有空间,若没有则从新实例化一个空间更大的新数组,把旧数组的数据拷贝到新数组中。 第二步:在新数组的最后一位元素添加值。数据结构
ArrayList在执行中间插入操做时: 第一步:先判断下标是否越界。 第二步:扩容。 第三步:若插入的下标为i,则经过复制数组的方式将i后面的全部元素,日后移一位。 第四步:新数据替换下标为i的旧元素。 删除也是同样:只是数组往前移了一位,最后一个元素设置为null,等待JVM垃圾回收。架构
从上面的源码分析,咱们能够获得一个结论和一个疑问。 结论是:ArrayList快在下标定位,慢在数组复制。 疑问是:可否将每次扩容的长度设置大点,减小扩容的次数,从而提升效率?其实每次扩容的长度大小是颇有讲究的。若扩容的长度太大,会形成大量的闲置空间;若扩容的长度过小,会形成频发的扩容(数组复制),效率更低。并发
LinkedList 是基于双向链表实现的非线程安全的集合,它是一个链表结构,不能像数组同样随机访问,必须是每一个元素依次遍历直到找到元素为止。其结构的特殊性致使它查询数据慢。
// 查询元素 public E get(int index) { checkElementIndex(index); // 检查是否越界 return node(index).item; } Node<E> node(int 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; } } // 插入元素 public void add(int index, E element) { checkPositionIndex(index); // 检查是否越界 if (index == size) // 在链表末尾添加 linkLast(element); else // 在链表中间添加 linkBefore(element, node(index)); } void linkBefore(E e, Node<E> succ) { final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
从源码中能够得知,
LinkedList在执行查询操做时: 第一步:先判断元素是靠近头部,仍是靠近尾部。 第二步:若靠近头部,则从头部开始依次查询判断。和ArrayList的elementData(index)
相比固然是慢了不少。
LinkedList在插入元素的思路: 第一步:判断插入元素的位置是链表的尾部,仍是中间。 第二步:若在链表尾部添加元素,直接将尾节点的下一个指针指向新增节点。 第三步:若在链表中间添加元素,先判断插入的位置是否为首节点,是则将首节点的上一个指针指向新增节点。不然先获取当前节点的上一个节点(简称A),并将A节点的下一个指针指向新增节点,而后新增节点的下一个指针指向当前节点。
Vector 的数据结构和使用方法与ArrayList差很少。最大的不一样就是Vector是线程安全的。从下面的源码能够看出,几乎全部的对数据操做的方法都被synchronized关键字修饰。synchronized是线程同步的,当一个线程已经得到Vector对象的锁时,其余线程必须等待直到该锁被释放。从这里就能够得知Vector的性能要比ArrayList低。
若想要一个高性能,又是线程安全的ArrayList,可使用Collections.synchronizedList(list);
方法或者使用CopyOnWriteArrayList集合
public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); } public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; } public synchronized boolean removeElement(Object obj) { modCount++; int i = indexOf(obj); if (i >= 0) { removeElementAt(i); return true; } return false; }
在这里咱们先简单了解一下CopyOnWrite容器。它是一个写时复制的容器。当咱们往一个容器添加元素的时候,不是直接往当前容器添加,而是先将当前容器进行copy一份,复制出一个新的容器,而后对新容器里面操做元素,最后将原容器的引用指向新的容器。因此CopyOnWrite容器是一种读写分离的思想,读和写不一样的容器。
应用场景:适合高并发的读操做(读多写少)。若写的操做很是多,会频繁复制容器,从而影响性能。
CopyOnWriteArrayList 写时复制的集合,在执行写操做(如:add,set,remove等)时,都会将原数组拷贝一份,而后在新数组上作修改操做。最后集合的引用指向新数组。
CopyOnWriteArrayList 和Vector都是线程安全的,不一样的是:前者使用ReentrantLock类,后者使用synchronized关键字。ReentrantLock提供了更多的锁投票机制,在锁竞争的状况下能表现更佳的性能。就是它让JVM能更快的调度线程,才有更多的时间去执行线程。这就是为何CopyOnWriteArrayList的性能在大并发量的状况下优于Vector的缘由。
private E get(Object[] a, int index) { return (E) a[index]; } public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } } private boolean remove(Object o, Object[] snapshot, int index) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] current = getArray(); int len = current.length; ...... Object[] newElements = new Object[len - 1]; System.arraycopy(current, 0, newElements, 0, index); System.arraycopy(current, index + 1, newElements, index, len - index - 1); setArray(newElements); return true; } finally { lock.unlock(); } }
更多Java源码、数据库、spring boot、内功心法等知识都是须要不断学习、积累、实践方能领会。所以我给你们推荐一个Java架构群:895244712
,里面有分布式,微服务,性能优化等技术点底层原理的视频,也有众多想要提高的小伙伴讨论技术,欢迎你们加群一块儿交流学习。
回到文章的标题,若是面试官问你ArrayList和LinkedList有什么区别?
ArrayList和LinkedList都不是线程安全的,小并发量的状况下可使用Vector,若并发量不少,且读多写少能够考虑使用CopyOnWriteArrayList。 由于CopyOnWriteArrayList底层使用ReentrantLock锁,比使用synchronized关键字的Vector能更好的处理锁竞争的问题。
相信这个回答可以很好的帮你经过面试:p