LinkedList源码解析
简介
优缺点:
优点:对于频繁的插入、删除操作,使用它比使用ArrayList效率高。
缺点:进行查询操作时效率低。
LinkedList进行查询操作时使用折半查找。
底层原理:
LinkedList底层使用双向链表存储数据。
LinkedList的继承与实现(该图由Idea自动生成):
LinkedList 继承于AbstractSequentialList,这个抽象类实现了get()、set()、add() 和 remove()这些函数。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
源码解析
使用无参构造函数创建一个LinkedList,使用Ctrl+鼠标左键进入查看源码,可以看出LinkedList的无参构造函数没有写任何的逻辑。
所以linkedList在创建对象时没有对属性进行初始化。
LinkedList的属性
// linkedList内存放元素的大小
transient int size = 0;
// 指向第一个节点的指针,头节点
transient Node<E> first;
// 指向最后一个节点的指针,尾节点
transient Node<E> last;
Node类
Node类是LinkedList内的私有的静态内部类,可以在LinkedList内直接调用,但不能在外部调用。
链表由一个或多个节点组成,Node是链表内的一个节点
private static class Node<E> {
// item为Node内存放元素的属性
E item;
// next指向下一个节点
Node<E> next;
// prev指向上一个节点
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Node节点图示
链表图示
创建LinkedList对象
1、使用无参构造函数
public LinkedList() {
}
可以看出,LinkedList()内没有任何逻辑,所以只有在添加第一个数据时,链表才会创建,linkedList内的属性size,prev,next此时皆为空。
2、使用有参构造函数
此时参数为Collection类,Collection类为LinkedList的父辈。
LinkedList(Collection<? extends E> c)函数
public LinkedList(Collection<? extends E> c) {
// this()方法调用无参构造函数创建linkedList对象
this();
// 向其中添加全部数据
addAll(c);
}
addAll(Collection<? extends E> c)函数
public boolean addAll(Collection<? extends E> c) {
// size为LinkedList大小,此时为0
return addAll(size, c);
}
addAll(int index, Collection<? extends E> c)函数
// index此时为从第index处节点开始添加数据
public boolean addAll(int index, Collection<? extends E> c) {
// 范围检查,判断是否越界,否则抛出异常
checkPositionIndex(index);
// Collection对象转为数组
Object[] a = c.toArray();
// 获取数组的长度
int numNew = a.length;
// 如果数组长度为0,插入数据失败
if (numNew == 0)
return false;
// 前置节点,后置节点
Node<E> pred, succ;
// index == size时表示从链表尾部添加元素,所以前置节点为当前链表的尾节点。
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);
// 前置节点为null,表示为头节点(first)
if (pred == null)
first = newNode;
else
// last.next = newNode
pred.next = newNode;
// last = newNode
// 令链表的最后一个节点等于新添加的节点
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
// 修改次数+1
modCount++;
return true;
}
向LinkedList集合中添加元素
1、add(E e)函数
public boolean add(E e) {
linkLast(e);
return true;
}
linkLast(E e)函数
void linkLast(E e) {
// 指定节点l指向last节点
final Node<E> l = last;
// 创建一个newNode节点,它的前置节点为l,后置节点为null
final Node<E> newNode = new Node<>(l, e, null);
// 指定last节点指向newNode节点
last = newNode;
// 此时头节点为空,所以第一个添加的节点为头节点
if (l == null)
first = newNode;
else
// 头节点不为空,则令最后一个节点指向这个新节点。
l.next = newNode;
// linkLast的长度+1
size++;
// 修改次数+1
modCount++;
}
注意:向linkedList中添加、插入、删除数据时,主要是令前置节点与后置节点建立联系,后置节点与前置节点建立联系,二者并不冲突。
2、add(int index, E element)函数
功能:向集合中指定位置插入元素
public void add(int index, E element) {
// 检查index范围是否越界
checkPositionIndex(index);
// 如果集合长度等于插入位置,则直接在链表尾部插入
if (index == size)
linkLast(element);
// 否则,查询位置在index处的节点,将新节点插入node(index)节点之前
else
linkBefore(element, node(index));
}
linkBefore(E e, Node succ)函数
void linkBefore(E e, Node<E> succ) {
// 当前节点的前置节点
final Node<E> pred = succ.prev;
// 就是使newNode插入pred节点与succ节点之间,并与pred与succ之间建立联系
final Node<E> newNode = new Node<>(pred, e, succ);
// 使succ节点与newNode节点建立联系,newNode为succ的前置节点
succ.prev = newNode;
// 如果前置节点为空,则新插入的节点成为头节点
if (pred == null)
first = newNode;
else
// pred与newNode建立联系
pred.next = newNode;
size++;
modCount++;
}
从LinkedList中删除数据
remove(int index)函数
public E remove(int index) {
// 检查index是否越界
checkElementIndex(index);
// 使用node(index)获取节点,然后使用下面的方法删除节点
return unlink(node(index));
}
unlink(Node x)函数
E unlink(Node<E> x) {
// 获取要移除的节点的元素
final E element = x.item;
// 获取后置节点
final Node<E> next = x.next;
// 获取前置节点
final Node<E> prev = x.prev;
// 如果前置节点为空,则令头节点指向后置节点next
if (prev == null) {
first = next;
} else {
// 令前置节点的下一个指向后置节点,去除与节点x的联系
prev.next = next;
// 使用垃圾回收机制
x.prev = null;
}
if (next == null) {
last = prev;
} else {
后置节点的上一个节点变为前置节点,去除与节点x的联系
next.prev = prev;
// 删除节点的上一个节点变为空 告诉给gc(垃圾回收机制)实现回收
x.next = null;
}
// 删除节点的item变为null,告诉给gc实现回收
x.item = null;
// 集合的长度减一
size--;
// 修改次数+1
modCount++;
return element;
}
使用index获取元素
get(int index)函数
public E get(int index) {
// 检查范围是否越界
checkElementIndex(index);
// 调用node函数返回节点的item
return node(index).item;
}
node(int index)
Node<E> node(int index) {
// 使用二分查找获取节点,只进行一次二分查找
// size >> 1 等同于 size / 2;
if (index < (size >> 1)) {
// 获取头节点
Node<E> x = first;
// for循环遍历链表从0到index。
// 从头开始正序遍历知道到达index
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的遍历
1、使用for循环遍历
2、使用增强for循环遍历
3、使用迭代器遍历
LinkedList可以作为队列、双端队列、栈使用
栈:先进后出。
队列:队列(queue)是一种是相对于栈的一种数据结构,它是先进先出(First In First Out)。它只可以在尾部添加元素。
双端队列(Deque:double ended queue):是一种相对于队列的一种数据结构。它可以在尾部和头部插入、移除和获取。
相对于普通队列,双端队列的入队和出队操作在两端都可进行。
栈(stack)、队列(queue)、双端队列(deque)
*栈和队列只是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,使用不同的名称和方法,概念上更为清晰。看看Deque接口里面的方法*
该图转载自博客:LinkedList实现栈、队列或者双端队列分析
总结
LinkedList本质上与ArrayList的实现方式差不多,但ArrayList底层是操作数组,而LinkedList的底层是操作双向链表。