一句话概括区别
1、ArrayList
ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。
数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数
组的数据复制到新的存储空间中。
当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
2、LinkedList
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较
慢。
另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆
栈、队列和双向队列使用。
详细分析
1、继承关系图
ArrayList 和LinkedList都实现了List接口,LinkedList还实现了Queue队列接口,队列是一种特殊的线性表。Queue:一个队列就是一个先进先出(FIFO)的数据结构。
2、数据结构上的对比
- ArrayList 基于动态数据的数据结构,数组是连续的内存存储,下标从0开始,查找快速,插入慢。定义数组时,数组长度已定,查找某个元素,直接访问所在下标即可,插入元素的话,就要把所插入位置后的元素都向后移动。删除原理一样,也有元素移动。
构造函数:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
我们来看一下数组的数据结构
- LinkedList 是一个双向链表、链表插入元素时,只需要对指针进行修改即可,而数组要移动数据来填补被删除的对象的空间。
我们来看一下双向链表的数据结构
3、性能分析。
源码分析(增删)
- 对于随机访问get和set,ArrayList优于LinkedList,因为ArrayList可以随机定位,而LinkedList要移动指针一步一步的移动到节点处。
- 对于新增和删除操作add和remove,LinkedList比较占优势,只需要对指针进行修改即可,而ArrayList要移动数据来填补被删除的对象的空间。
ArrayList
(1)数组末尾插入元素,ArrayList只需要容量加一,在数组最后插入元素即可
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 容量+1
elementData[size++] = e;
return true;
}
(2)数组中插入元素,ArrayList容量加一,复制到一个新数组,在插入位置插入元素。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // 容量+1
System.arraycopy(elementData, index, elementData, index + 1,
size - index);//复制到一个新的数组
elementData[index] = element;
size++;
}
(3)删除
如果删除的元素在数组中,和插入元素一样,需要复制到一个新的数组,如果删除的元素在数组末尾,不执行复制到新数组的代码。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);// 准备删除的元素
int numMoved = size - index - 1;//开始移动的位置,
//如果删除的元素在队尾,不执行if分支
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//复制到一个新数组,不包括删除的数组
elementData[--size] = null; // 清空最后一位,告诉GC回收
return oldValue;//返回删除的元素
}
LinkedList
Node节点一共有三个属性:item代表节点值,prev代表节点的前一个节点,next代表节点的后一个节点。每个结点都有一个前驱和后继结点,并且在 LinkedList中也定义了两个变量分别指向链表中的第一个和最后一个结点。
/**
* Pointer to first node.指向第一个节点
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.指向最后一个节点
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
...
private static class Node<E> {
E item;//节点值
Node<E> next;//后一个节点
Node<E> prev;//前一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
(1)插入新元素
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);//增加到队尾
else
linkBefore(e, next);//增加到队中
nextIndex++;
expectedModCount++;
}
队尾
定义节点的前驱后继,链表不为空的情况下,直接把最后一个节点的next指向新节点
void linkLast(E e) {
final Node<E> l = last;//最后一个节点
// 参考下面Node节点,定义时,已经把前后节点定义好。
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;//新节点赋值给队尾
// 链表为null的情况下,赋值给第一个节点,否则赋值给下一个节点
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
}
队中
定义节点的前驱后继,在链表不为空的情况下,把插入位置的前一个节点的next指向新节点。
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;//succ的前节点
// 参考下面Node节点,定义时,已经把前后节点定义好。
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;//新节点赋值给succ的前节点
// succ处于第一个节点,把新节点赋给第一节点
if (pred == null)
first = newNode;
else
pred.next = newNode;//succ不是第一个节点,把succ的next指向新节点
size++;
modCount++;
}
(2)删除
目标元素为头节点,将链表第一个节点指向删除的元素的后继;
尾节点,将链表最后一个节点前驱指向删除的元素的前驱;
中间节点,前一元素的后继指向目标元素的后继,将前一元素的前驱指向目标元素的前驱。
public boolean remove(Object o) {
if (o == null) {
// 遍历目标节点
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 遍历目标节点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
/**
* Unlinks non-null node x. 不为null的节点
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;//删除的目标元素
final Node<E> next = x.next;//后继
final Node<E> prev = x.prev;//前驱
// 前驱为nulll,说明为头节点,将链表第一个节点指向删除的元素的后继
// 不是头节点,将前一元素的后继指向目标元素的后继
// 目标元素的前驱置null
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 后继为nulll,说明为尾节点,将链表最后一个节点前驱指向删除的元素的前驱
// 不是尾节点,将前一元素的前驱指向目标元素的前驱
// 目标元素的后继置null
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// 目标元素置null,链表长度减1,modCount自加
x.item = null;
size--;
modCount++;
return element;
}
时间复杂度
操作 | 数组 | 链表 |
---|---|---|
随机访问 | O(1) | O(N) |
头部插入 | O(N) | O(1) |
尾部插入 | O(N) | O(1) |
头部删除 | O(1) | O(1) |
尾部删除 | O(1) | O(1) |
一句话总结
查询用ArrayList,插入删除用LinkedList。