LinkedList学习笔记
一、前言
前面文章写了关于ArrayList的源码解读,今天也正好把LinkedList一些方法的源码也研究一下。
二、LinkedList特点
基于双向列表,查询速度慢,增删改速度快
三、LinkedList的继承实现关系
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
- LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
- LinkedList 实现 List 接口,能对它进行队列操作。
- LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
- LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
- LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
- LinkedList 是非同步的。
四、LinkedList类属性和Node节点
//记录链表实际长度
transient int size = 0;
//记录整条链表的头结点
transient Node<E> first;
//记录整条链表的尾节点
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;
}
}
五、LinkedList构造方法
五、LinkedList构造方法
public LinkedList() {
}
//使用一个集合来当做参数的构造器
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
//检查插入的位置是否合法
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
//数组的长度为0直接返回
if (numNew == 0)
return false;
//创建前驱节点和后继节点
Node<E> pred, succ;
if (index == size) {
//插入的位置是链表尾部,后继节点置空
succ = null;
//前驱节点就是尾结点
pred = last;
} else {
//插入位置为其他的某个位置时,寻找该节点指向后继节点
succ = node(index);
//该节点的后继节点的前驱节点指向前驱节点
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//构造一个后继节点为null的节点
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
//前驱节点为null,说明在头节点之前插入,newNode指向头节点
first = newNode;
else
//不是头节点之前插入的,newNode节点就是前驱节点的后继节点
pred.next = newNode;
//newNode就是前驱节点
pred = newNode;
}
//同理
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
六、add、offer基础方法分析
//从头部增加
private void linkFirst(E e) {
//获取头节点
final Node<E> f = first;
//构建一个prev值为null,next为f,内容为e的节点
final Node<E> newNode = new Node<>(null, e, f);
//将newNode作为首节点
first = newNode;
if (f == null)
//f为null,说明是原链表是空链表,newNode节点既是头节点也是尾结点
last = newNode;
else
//如果不为null,newNode节点就为原链表的上一节点
f.prev = newNode;
size++;
modCount++;
}
//从尾部增加
public void linkLast(E e) {
//获取尾节点
final Node<E> l = last;
//构造一个以尾部为前驱节点创建一个新节点
final Node<E> newNode = new Node<>(l, e, null);
//newNode作为尾结点
last = newNode;
if (l == null)
////l为null,链表为空,newNode节点既是头节点也是尾结点
first = newNode;
else
//当前节点为原链表尾结点的下一节点
l.next = newNode;
//容量加一
size++;
//修改操作数+1
modCount++;
}
//在一个非空节点之前插入一个节点
void linkBefore(E e, Node<E> succ) {
//获取当前节点的前置节点
final Node<E> pred = succ.prev;
//构建一个新的节点
final Node<E> newNode = new Node<>(pred, e, succ);
//succ的前置节点改为当前节点
succ.prev = newNode;
if (pred == null)
//succ的前置节点为空,newNode设置为头节点
first = newNode;
else
//succ的前置节点不为空,newNode设置为succ前置节点的下一节点
pred.next = newNode;
size++;
modCount++;
}
结论:增加都是在头部、中间、尾部增加一个双端节点的操作。但是需要注意添加时候对头节点和尾结点为空的处理。final Node
七、查找方法分析
//根据索引位置取值
public E get(int index) {
//检查索引的边界是否在0-size之间
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 E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
结论:获取头节点和尾结点的时候如果链表为null都是会抛出异常,element()、peek()、peekFirst()、peekLast()。这些方法都大致相同
八、poll、remove方法分析
//删除指定对象,都是从前向后遍历,找到匹配对象调用unlink()方法
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;
}
E unlink(Node<E> x) {
//获取当前节点的内容,前驱节点和后继节点
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;
}
//内容置null
x.item = null;
size--;
modCount++;
return element;
}
//删除该节点需要将当前节点的内容前驱节点后继节点置空,并且需要将该节点的前驱节点的后继节点指向该节点的后句节点,该节点的后继节点也是同样的道理。
//根据索引删除,先判断index的范围
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//删除头节点
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
final E element = f.item;
//获取该节点的后继节点
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
//头节点执行该节点的后继节点
first = next;
if (next == null)
//如果next为空,说明链表已经为null了,将尾结点设置为null
last = null;
else
//否则将该节点后继节点的前驱节点置空
next.prev = null;
size--;
modCount++;
return element;
}
//删除尾结点 和删除头节点是一样的道理
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
// poll和pollFirst调用的方法相同
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
//他们三个调用的方法都是unlinkFirst,unlinkLast。上面也已经说过了,这里就不做累述了
九、总结
LinkedList存储的元素就是一个一个的节点,每个节点记录着前面和后面节点的信息。所以在查询的时候需要前置或者后继以此查找,ArrayList根据下边直接查找,但是删除的时候ArrayList需要移动元素然后置空,但是LinkedList只需要将修改前驱和后继节点,然后置空当前节点的内容就行了。