ArrayList与LinkedList区别
基本结构
-
ArrayList
transient Object[] elementData;
-
LinkedList
class Node<E> { E item;//存储的元素 LinkedList.Node<E> next;//指向下一个节点的指针 LinkedList.Node<E> prev;//指向上一个节点的指针 }
首先我们都知道,ArrayList是基于动态数组实现的数据结构
那么动态指的什么呢?
这里的动态指的是数组的大小是可以动态调整的,那么具体是怎么实现的呢?
下面,我们基于部分源码进行分析
自动增容
//默认大小为10
private static final int DEFAULT_CAPACITY = 10;
//ArrayList扩容的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 动态扩容实现
* @param minCapacity
*/
private void grow(int minCapacity)
{
int oldCapacity = elementData.length;
//新数组大小为原大小的1.5倍(源码中使用了移位运算法,右移一位相当于0.5倍,1+0.5就是原来的1.5倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容后容量还是小于需要的最小容量,则会以需要的最小容量作为数组最终的容量
if (newCapacity - minCapacity < 0)
{
newCapacity = minCapacity;
}
//如果扩容后的容量大于最大容量,将会以最大容量作为数组最终的容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
{
newCapacity = hugeCapacity(minCapacity);
}
//复制原数组内容到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
经过分析源码,我们可以发现,ArrayList默认数组大小为10,如果插入元素后容量超出数组当前的最大容量,就会触发自动扩容机制,自动扩容机制将创建一个新的数组(大小为原数组的1.5倍),然后将原数组的内容通过Arrays.copyOf复制到扩容后的新数组**(这是添加方法费时的关键)**。
但是对于LinkedList来说,由于其基于双向链表实现,不存在增容问题,在插入元素时只需要创建新的节点对象,拼接到尾结点即可。
给末尾添加元素
-
ArrayList
/** * 给数组尾添加元素 * @param e * @return */ public boolean add(E e) { ensureCapacityInternal(size + 1); //这里进行增容检测,如果容量不足将会自动增容,耗时的关键 elementData[size++] = e;//将元素添加到末尾 return true; }
这里我们就可以发现,当ArrayList容量足够大时,给数组尾部添加元素的操作效率将非常高。
-
LinkedList
/** * 给尾部添加新元素 * @param e */ void linkLast(E e) { //尾结点位置 final LinkedList.Node<E> l = last; //将插入的元素构建成新的节点 final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null); //尾结点指向当插入的节点 last = newNode; if (l == null) { first = newNode; } else { l.next = newNode; } size++; modCount++; }
同样,尾插时效率也很高,但是由于ArrayList存在动态增容的可能,所以平均情况下,添加效率LinkedList较高。
给指定位置添加元素
-
ArrayList
/** * 给指定位置添加元素 * @param index * @param element */ public void add(int index, E element) { rangeCheckForAdd(index);//首先检查插入位置的下标是否合法(下标在0~size之间合法) ensureCapacityInternal(size + 1); // 进行动态增容判断 //将插入位置的元素及其之后元素整体后移(这里也需要耗时) System.arraycopy(elementData, index, elementData, index + 1, size - index); //插入新元素到指定位置 elementData[index] = element; //大小+1 size++; } /** * 检查插入位置的下标是否合法 * @param index */ private void rangeCheckForAdd(int index) { if (index > size || index < 0) { throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } }
当向数组指定位置插入元素时,其需要向后移动大量数据位置(复制操作),所以相对尾插来说效率较低。
删除元素
public E remove(int index)
{
rangeCheck(index);//检查下标是否越界
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
//向前复制
if (numMoved > 0)
{
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null; //释放最后一个位置,方便GC处理
return oldValue;
}
删除元素和给指定位置插入元素类似,需要向前移动大量元素,这里就不进行过多赘述。
相比之下,LinkedList只需改变指针指向无需移动其他元素位置,所以在插入和删除方面,LinkedList效率也往往高于ArrayList
查找元素
-
ArrayList
public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index];//直接根据下标获取 }
-
LinkedList
public E get(int index) { checkElementIndex(index); return node(index).item; } //遍历链表查找 LinkedList.Node<E> node(int index) { if (index < (size >> 1))//如果下标位置小于链表长度的一半,则从前向后查找 { LinkedList.Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else//如果下标位置大于等于链表长度的一半,则从后向前查找 { LinkedList.Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
经过源码分析,我们可以发现,ArrayList的访问基于数组下标,其速度将非常快,但是LinkedList最坏情况下需要遍历一半长度的链表才能找到,所以综上:ArrayList的访问效率要远远高于LinkedList
总结
插入方面:
- 默认情况下,比如调用ArrayList和LinkedList的add(e)方法,都是插入在最后,如果这种操作比较多,那么就用LinkedList,因为不涉及到扩容。
- 如果调用ArrayList和LinkedList的add(index, e)方法比较多,就要具体考虑了,因为ArrayList可能会扩容,LinkedList需要遍历链表,这两种到底哪种更快,是没有结论的,得具体情况具体分析。
- 如果是插入场景比较少,但经常需要查询的话,查询分两种,第一种就是普通遍历,也就是经常需要对List中的元素进行遍历,那么这两种是区别不大的,遍历链表和遍历数组的区别,第二种就是经常需要按指定下标获取List中的元素,如果这种情况如果比较多,那么就用ArrayList。
查询方面
- 对于ArrayList,它可以按照数组下标快速的定位要查询元素,相比之下,LinkedList需要对底层链表进行遍历,才能找到指定下标的元素。
- 如果我们讨论的是获取第一个元素,或最后一个元素,ArrayList和LinkedList在性能上是没有区别的,因为LinkedList(双端队列)中有两个属性分别记录了当前链表中的头结点和尾结点,并不需要遍历链表。
删除方面及指定位置插入
- 对于ArrayList来说,其需要移动大量元素,这将浪费太多时间(插入时还有可能出现自动扩容问题,不但会影响时间也可能浪费空间)
- 对于LinkedList来说,只需要移动指针指向即可。
内存占用方面
-
一般情况下,由于LinkedList使用链表结构存储,其每个节点都需要单独的内存保存当前节点的前驱结点及后继节点,所以其内存占用应该较高。
-
但是这也不是绝对的,如果数据量刚好超过了ArrayList的容量,将会触发自动增容机制(比如存储11个数据,在第存储第11个时将触发自动增容机制,容量将变为15,将有4个被浪费),这将浪费将近一半的容量。
所以,如果数据量较大时,并且需要添加元素的话,LinkedList占用的空间不一定比ArrayList占用的空间小~
-
如果需要对集合进行序列化操作,由于 ArrayList使用**transient关键字修饰,其中没有被使用的空间将不会被序列化**,这种情况下,ArrayList的空间相比LinkedList可能会少一些。