LinkedList源码分析笔记
查看以往的文章空易缘的博客
本文是java集合源码分析第三篇,上一篇Vector源码分析笔记,本文分析LinkedList,List集合的另一实现类,同时也可做为队列的实现类,通过双向链表存储数据
自己学习总结的笔记,有写的不对的地方,还望大佬们多多指教,我会及时修正
概述
- 继承自AbstractSequentialList抽象类,实现了List、Deque、Cloneable、Serializable接口
- 基于双向链表实现的集合,可以做为双向队列的实现类
- 不支持随机访问,没有实现RandomAccess接口,查找元素效率较低
- 插入和删除操作效率较高,没有扩容操作,也不需要预留空间
源码分析
字段信息
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;
}
内部节点类
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;
}
}
构造方法
//空的构造方法,啥也没干
public LinkedList() {
}
//通过addAll()方法将传入的集合所有元素加入到链表中
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
内部方法–根据index查找元素
如果index在链表前半段,则从链表头节点开始遍历查找;index在后半段,则从链表尾节点开始查找,提高查找效率
/**
*提高查找效率的查找方法
*/
Node<E> node(int index) {
// 判断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;
}
}
添加单个元素
/**
*添加单个元素,只需要将尾节点的后继节点指针指向e即可
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
*在尾部插入一个元素
*/
void linkLast(E e) {
//保存当前的尾部节点
final Node<E> l = last;
//创建一个新的节点,该节点值为e,前驱指针指向当前的尾部节点,后继节点指向null
final Node<E> newNode = new Node<>(l, e, null);
//更新尾节点指针
last = newNode;
//如果当前插入的是第一个节点,则头节点也指向newNode,
if (l == null)
first = newNode;
//如果不是,则要更新上一个尾节点的后继指针
else
l.next = newNode;
size++;
//记录修改次数
modCount++;
}
/**
*在指定位置添加元素
*/
public void add(int index, E element) {
//判断index是否合法0<=index<=size
checkPositionIndex(index);
//如果index=size,也就相当于在链表尾部插入节点
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// 获取目标节点的前驱节点
final Node<E> pred = succ.prev;
//创建新节点,新节点的前驱指针指向原节点的前驱节点,后继指针指向原节点
final Node<E> newNode = new Node<>(pred, e, succ);
//更新原节点的前驱节点,原节点的前驱点的前驱指针指向新节点
succ.prev = newNode;
//判断是否是第一次插入,第一插入需要更新链表头节点指针
if (pred == null)
first = newNode;
else
//更新前驱节点的,后继指针
pred.next = newNode;
size++;
modCount++;
}
在指定位置插入节点图示:
添加多个元素
/**
*不指定位置插入多个元素(相当于在链表尾部插入多个元素)
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
/**
*在index处插入多个元素
*/
public boolean addAll(int index, Collection<? extends E> c) {
//判断index是否合法0<=index<=size
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//判断插入的位置,获取插入节点的信息
//pred表示插入位置的前驱节点,succ表示插入位置节点
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;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)//链表中没有节点,则需要更新链表头节点
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
//将插入的一段链表与原链表插入位置后一段连接起来
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
删除元素
//根据下标删除节点
public E remove(int index) {
checkElementIndex(index);//判断下标合法性
return unlink(node(index));
}
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;
//前驱节点为null,则表明要删除节点为链表头节点
if (prev == null) {
first = next;
} else {
//双向链表删除节点,只需要将需删除节点的前驱节点的后继指针指向自己的后继节点即可
prev.next = next;
//断开删除节点前驱指针与链表的连接
x.prev = null;
}
//后继节点为null,表明删除的是链表的尾节点
if (next == null) {
last = prev;
} else {
//更新删除节点的后继节点的前驱指针
next.prev = prev;
//断开删除节点后继指针与链表的连接
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
删除节点图示:
查询元素
public E get(int index) {
checkElementIndex(index);//判断下标合法性
return node(index).item;//遍历链表,一个个查找符合下标的节点
}
修改元素
public E set(int index, E element) {
checkElementIndex(index);//判断下标合法性
Node<E> x = node(index);//根据下标查找需要修改值得节点
E oldVal = x.item;//保存节点值返回
x.item = element;//覆盖原节点的值
return oldVal;
}
总结
- 为了提高根据index查找元素的速度,会判断index位于链表前半段还是后半段,前半段则从链表头节点开始查找,后半段则从尾节点开始查找
- 当内部链表元素个数发生改变时,modCount++,记录结构修改次数
- 双向链表不支持随机访问,所有跟index相关的操作,都只能是遍历,挨个查找
- 插入元素和删除元素效率较高,只需要找到相关节点,修改相关节点的前驱指针和后继指针即可
- 批量添加元素时,会利用toArray()将传入的集合转化为数组,再利用循环依次添加