LinkedList源码解析
源码基于java8
LinkedList整体结构
LinkedList是实现了List接口和Deque接口,他的结构类似于双端链表。
实现Cloneable接口表示节点可以被浅拷贝,实现了Serializable接口代表可被序列化。
LinkedList是线程不安全的,如果想变成线程安全的可以使用Collections中的
synchronizedList方法。
我们可以先看一下node的组成结构
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;
}
}
从源码中我们知道,链表中的每个节点称为Node,Node都有pre和next属性。
//默认链表大小是0
transient int size = 0;
/**
* 指向第一个Node
* first必须满足他的prev是null并且他自身不是null
*
*/
transient Node<E> first;
/**
last的next是null,并且自身不是null
*/
transient Node<E> last;
/**
* 构造方法
*/
public LinkedList() {
}
当链表中没有数据时,first和last是同一个节点,前后都指向null。
所以可以结构图可以是这样(自己画的比较丑,别介意)
ps: 在Java8中已经不是循环链表了,只是双向链表。
新增
追加节点时,可以从头部追加也可以新增到链表尾部,默认是追加到链表尾部,add方法是追加尾部,addFirst是从头部开始追加。
先看add方法
public boolean add(E e) {
linkLast(e);
return true;
}
//追加在链表最后
void linkLast(E e) {
//last是最后一个节点
final Node<E> l = last;
//因为追加到链表最后所以新节点的next=null
final Node<E> newNode = new Node<>(l, e, null);
//用新节点替换掉旧的last节点
last = newNode;
//旧的last是null,代表是第一次添加,所以新节点就是first
if (l == null)
first = newNode;
else
//否则,last.next=newNode
l.next = newNode;
//最后链表长度自增1,修改版本数加1
size++;
modCount++;
}
从头部追加(addFirst方法)
//在链表头部增加
public void addFirst(E e) {
linkFirst(e);
}
//连接新元素,新元素作为first
private void linkFirst(E e) {
//首选记录下,上次的first
final Node<E> f = first;
//新建一个节点,因为追加在链表头,所以前驱是null,后继是上次的first
final Node<E> newNode = new Node<>(null, e, f);
//然后新节点作为现在的first
first = newNode;
//如果是第一次增加在链头,那么就是last节点
if (f == null)
last = newNode;
else
//否则旧的first的前驱节点就是当前新增的节点
f.prev = newNode;
//链表长度++,版本++
size++;
modCount++;
}
删除
链表的节点删除和新增方式类似,可从尾部删除也可以从头部删除,删除会把节点的值前后节点都设置成null,方便GC回收。
根据节点值删除
public boolean remove(Object o) {
//删除的节点是null
if (o == null) {
//从头结点开始循环遍历,直到遇到第一个节点值是null的,删除
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
//删除并返回true
unlink(x);
return true;
}
}
} else {
//如果要删除的元素不是null,还是需要一次遍历节点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
//通过equals方法判断是不是将要删除的节点
unlink(x);
return true;
}
}
}
//如果将要删除的节点不在链表中会返回false
return false;
}
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;
//然后将被删除的节点的前驱置为null,有助于GC更快回收该对象
x.prev = null;
}
//删除后继节点,如果后继节点是null,那代表是最后一个节点
if (next == null) {
//所以删除后,最后一个节点就是被删除节点的前驱
last = prev;
} else {
//如果被删除的节点的后继不是null,那么后继节点的前驱就是被删除节点的前驱
next.prev = prev;
//最后需要将被删除的节点的后继指针指向null,也是帮助GC
x.next = null;
}
//最后将被删除节点设置为null,链表长度--,版本号++。
x.item = null;
size--;
modCount++;
//返回待删除的元素
return element;
}
删除指定位置的节点
public E remove(int index) {
//因为根据下标删除,所以需要检查一下是否发生越界
checkElementIndex(index);
return unlink(node(index));
}
//如果下标在这0~size就返回true,否则就会抛出 IndexOutOfBoundsException
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
//检查下标是否越界
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
删除链表头部的节点
//pop调用removeFirst,removeFirst调用unlinkFirst方法
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
//如果first不存在就不能删除,直接抛出NoSuchElementException异常
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
//具体删除还得看unlinkFirst方法
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//记录要删除的节点最终需要返回
final E element = f.item;
//first的后继节点
final Node<E> next = f.next;
//将被删除的节点的值置为null
f.item = null;
//这一步帮助GC
f.next = null;
//然后被删除的节点的后继节点现在就成了,first节点
first = next;
//如果此时他为null,那就是个空链表
if (next == null)
last = null;
else
//否则的话,next的前驱需要指向null,因为first的前驱就是null,他将要变成first
next.prev = null;
//最终链表长度-1,版本号+1
size--;
modCount++;
return element;
}
查询
LinkedList查询节点的速度是比较慢的,需要挨个循环查找。
根据链表索引位置查询节点
//根据元素下标,返回非空节点
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index处于队列的牵绊部分,从头开始查找,size>>1=size/2
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;
//循环到index的后一个节点
for (int i = size - 1; i > index; i--)
x = x.prev;
//返回node
return x;
}
}
在查找的时候,LinkedList并没有采用从头到尾进行循环的方法,而是根据二分查找的思想,缩小了查找范围。如果在链表的前半部分就从前开始查,如果在后半部分流从后往前查,这样做提高了一些性能。
因为LinkedList也实现了Deque接口,而Deque是继承自Queue接口的。
所以实现了Queue的一些方法。
这里进行简单的对比:
- 新增 add offer;二者底层实现相同。
- 删除 remove poll(e) 链表为空remove会抛出NoSuchElementException,poll会返回null
- 查找 element peek 链表为null,element会抛出NoSuchElementException异常,peek返回null。
总结
ArrayList和LinkedList的对比
- 两者底层实现的数据结构不同。ArrayList底层是动态数组实现,LinkedList底层是双向链表。
- ArrayList和LinkedList都是不同步的,也就是不能保证线程安全。如果有线程安全问题,会抛出CouncurrentModificationException的错误,意思是在当前环境中,数组合链表的结构已经被其它线程所修改。所以 换成CopyOnWriteArrayList并发集合类使用或者Collection#synchronized。
- LinkedList不支持随机元素访问,ArrayList支持。因为ArrayList实现了RandomAccess接口,这个接口只是一个标识接口,接口中什么都没定义,相当于空接口,在binarySearch方法中,要判断传入的List集合是否是这个接口的实现,如果实现了RandomAccess接口才能使用indexedBinarySearch方法,否则只能一个个顺序遍历,也就是调用iteratorBinarySearch方法。
- 空间占用上LinkedList每次操作的对象就是Node节点,这个Node中有前驱和后继,而ArrayList的空间消耗主要是他在数组尾部会预留一定的空余,所以LinkedList的空间消耗比ArrayList更多。
- 再来看新增和删除。ArrayList顺序插入到数组尾部,时间复杂度是O(1),如果是指定位置插入或删除的话,时间复杂度是O(n-i);i是插入/删除的位置,n代表长度。LinkedList插入和删除操作的是节点的前驱和后继,所以直接改变指向,时间复杂度都是O(1)。LinkedList在做新增和删除的时候,慢在寻找被删除的元素,快在改变前后节点的引用地址。而ArrayList在新增和删除的时候慢在数组的copy,快在寻找被删除/新增的元素。
以上有本人理解不到位的地方,欢迎各位指出,共同学习,共同进步!