ArrayList与LinkedList
前言
ArrayList和LinkedList是我们平常经常使用的两大数据结构,最主要的几个方法就是:add,get,remove和我们一般不感知的grow方法(扩容),grow方法是动态数组的核心方法,也是我们不需要去管理数组的长度就可以一直add的原因。
ArrayList
ArrayList底层是基于数组实现的,支持随机存取,也就是可以直接通过下标去拿到对应的值
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 初始容量大小
private static final int DEFAULT_CAPACITY = 10;
// 存放值的数组,所有add的值都是存放在elementData中
transient Object[] elementData;
// 记录当前数组的size
private int size;
}
add方法
public boolean add(E e) {
// 确保数组容量充足
ensureCapacityInternal(size + 1);
// 将值放入数组
elementData[size++] = e;
return true;
}
add方法一共做了两个事情:
- 确保数组容量充足
- 将值放入数组
elementData
如何确保的数组容量充足?
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
先执行calculateCapacity
方法计算需要的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果现在的elemntData数组是默认的空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 计算默认容量和最小所需容量哪个大,返回二者之间大的那个
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 如果elemntData数组不是默认的空数组,就返回最小所需容量
return minCapacity;
}
计算完所需容量之后,进入ensureExplicitCapacity
方法,判断是否需要扩容操作,如果最小所需容量大于现在的数组长度,就需要进行扩容
private void ensureExplicitCapacity(int minCapacity) {
//添加修改次数,每次对列表的结构进行修改的时候(添加元素或者删除元素)都会修改
modCount++;
// 根据前面方法返回的所需容量,来判断是否需要扩容,如果minCapacity > 现在的数组长度,就需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
get方法
public E get(int index) {
// 检查数组下标是否越界
rangeCheck(index);
// 返回elementData数组中对应下标的值
return elementData(index);
}
- 检查数组下标是否越界,越界会抛出
IndexOutOfBoundsException
异常 - 返回elementData数组中对应下标的值
分割了
grow方法
grow方法的调用时机就是add方法中确保数组容量是否充足的时候,如果数组容量小于所需容量,就会调用grow方法
private void grow(int minCapacity) {
// 计算老数组长度
int oldCapacity = elementData.length;
// 计算新数组长度
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 判断新数组长度是否在最大数组长度之中
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用jdk提供的Arrays.copyOf方法返回一个新长度的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
grow方法做了四个事情:
- 计算老数组长度
- 计算新数组长度
- 判断是否超过最大数组长度
- 用新数组给
elementData
赋值
新数组长度的计算
在计算新数组长度的时候,会有两个值进行比较,选取一个较大的值,两个值分别是oldCapacity + (oldCapacity >> 1)
和minCapacity
,前者是原数组长度的1.5倍,后者是我们传入的最小所需容量也就是elementData.length + 1
,一般情况下都会是原数组长度的1.5较大
什么情况下会分配minSize长度的数组给elementData?
比如一个数组有10个容量,这个时候来了一个16个元素的ArrayList,要把这15个元素的ArrayList全部add进前一个数组,这个时候前一个数组就会使用minSize = 10+15
去扩容,而不是10 + 10 >>> 1 = 15
为什么会存在一个数组最大长度?
数组的最大长度一般是Integer的MAX_VALUE - 8
,因为一些虚拟机存储了一些头部信息在数组中,尝试去分配更大的空间给一个数组会OutOfMemoryError
remove方法
remove方法是在一个ArrayList中删除一个指定下标上的元素
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;
return oldValue;
}
remove做了几件事情:
- 判断下标是否越界
- 添加修改次数
- 获取该下标的元素,为返回做准备
- 计算需要向前移动的元素的长度
- 使用arrayCopy达成移动数组的目的,将index+1下标开始的elementData
- 将elementData的index位置赋值为null,让GC去收集
这个arrayCopy移动数组的操作也就是ArrayList里删除操作中花费大量时间的原因,需要把每个index之后的元素向前移动一个单位。
LinkedList
LinkedList的底层实现就是链表,将一个个元素封装成一个Node,Node中包含元素的Value,指向一下个Node的next引用,以及指向前一个Node的prev引用,一般的链表都只有next引用,而LinkedList的Node里还保存了一个prev引用
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;
}
}
ArrayList:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
}
可以看到LinkedList里只保存了3个属性,分别是first头引用,last尾引用,以及长度size
add
在LinkedList的add默认添加到了尾部,也可以直接从头部添加,因为LinkedList是基于链表实现的,所以修改结构的操作都是非常快速的,只需要修改一下引用的对象即可
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
add操作做了几件事情:
- 获取尾引用
- 用传进来的e对象新建一个Node对象
- 将新建的Node对象赋值给last
- 判断是否原来的尾引用为空
4.1 为空则说明原来的List中无元素,修改头引用为当前引用进行初始化
4.2 不为空则将尾引用的next 赋值为 新建的Node对象 - 修改size
get方法
LinkedList提供了三种不同的get方法,分别是get(int index),getFirst()和getLast()
getFirst和getLast,这两个方法的时间复杂度是O(1),因为在LinkedList中保存过First和Last引用,直接把这两个引用返回出来即可。
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
如果是根据index去访问的话,就需要O(n)的时间复杂度了,因为LinkedList是通过链表来实现的,不可以做到像数组一样根据下标直接去计算出地址去获取值,必须通过Node.next去逐个访问。
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;
}
}
将两个方法联合起来看,可以发现LinkedList对取值操作还是进行过一些优化的,如果下标小于size/2,那么说明这个节点出现在数组的前半部分,就从头节点开始向后寻找,反之从尾节点开始向前寻找
grow方法
LinkedList是不存在grow方法的,因为LinkedList的实现是链表而不是数组,数组需要先分配一块连续的内存空间用来存储,而链表是产生一个个Node对象,这个Node对象在物理上是不要求连续的,而且也不需要预先分配空间,只有需要的时候再去分配一个空间给Node对象,将它串联进整个链表中
remove方法
LinkedList的remove操作,主要时间消耗在寻找这个要被删除的节点,因为LinkedList不支持随机存取,只能逐个遍历,当找到指定下标的节点的时候,调用unLink方法
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;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
在remove方法中,有两种特殊情况:
- 要删除的节点是头结点
- 要删除的节点是尾结点
如果要删除的是头结点,则first节点要向后移动一位
如果要删除的是尾结点,则last节点要向前移动一位
如果都不是,则将prev节点的next指向next,将next节点的prev指向prev
ArrayList和Linked的区别
- ArrayList底层是数组实现的,需要预先分配一块连续的空间;LinkedList底层是链表实现的,不需要预先分配空间
- ArrayList和LinkedList都可以实现数组动态扩展,但是实现是不一样的;ArrayList通过grow方法去维护一块足够大的内存空间来存储数据,而LinkedList不需要grow方法去维护一块内存空间,只需要在存储元素的时候,给Node分配一块内存空间,将Node串联进链表中就好
- ArrayList支持随机存取,可以直接使用下标去访问;LinkedList不支持随机存取,需要逐个遍历
- ArrayList删除操作的时候需要将被删除元素之后的所有元素都向前移动一个下标;LinkedList删除操作的时候只需要改变被删除元素前后节点的next和prev引用即可