LinkedList 数据结构
底层使用双向链表结构,即有一个头结点和尾结点,意味着可以从头开始正向遍历,或者从尾逆向遍历,并且可以直接针对头部和尾部进行操作
由于使用双向链表实现。那使用 双向链表 和 单链表有什么区别?
- 在删除单链表中某个结点时,一定要得到删除结点的前驱,得到前驱有两种方法,一种是在定位删除结点的同时一路保存当前结点前驱。第二种是在定位到待删除结点之后,重新从单链表表头开始来定位前驱。这两种方法效率一样,指针总的移动操作都会是 2*i 次。而如果使用双向链表,则不需要定位前驱结点,总的移动次数为 i 次
- 查找也一样,会比较 index 确定从 first 往 last 还是从 last 往 first。
在执行任何操作的时候都必须先遍历此列表来靠近 index 查找所需要的值,即顺序存取的,要区别随机存取这两个概念。
异步,非线程安全,但是Collections.synchronizedList方法可以实现线程安全的操作。
添加默认是尾插,按指定位置插入都是会比较 index 与 size>>1 大小而确定从 first 还是从 last 开始。几乎 里面所有涉及按位置遍历的都是这样实现的,只有按 Node 删除 和 查找 是需要从 first 到 last 遍历的。并
继承结构
从上图中得到一些信息:
- LinkedList:说明它支持泛型。
- implements List:说明它支持集合的一般操作。
- implements Deque:Deque,Double ended queue,双端队列。LinkedList可当作队列或双端队列。
- implements Cloneable:表明其可以调用clone()方法来返回实例的field-for-field拷贝。
- implements java.io.Serializable:表明该类是支持序列化,能够序列化传输。
- LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
另外,之前说 ArrayList 时只有四层,这里为什么要多一层 AbstractSequentialList 抽象类?
- 减少实现顺序存取(例如LinkedList) 这种类的工作,方便抽象出类似LinkedList类的共同方法
- 如果自己想实现顺序存取(链表形式), 就继承 AbstractSequentialList ;想实随机存取就实现 AbstractList 抽象类
- 这也是一种设计思想,越在底层的类越有自己的特点,越上层就越抽象和共性。
源码分析
类的属性
属性比较少,一个头结点,一个尾结点,一个表示链表中实际元素个数的变量。结点都有 transient 修饰,意味着该域是不会序列化的。
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;
}
构造方法
无参构造
// Constructs an empty list.
public LinkedList() {
}
带参构造
//将集合c中的各个元素构建成LinkedList链表。
public LinkedList(Collection<? extends E> c) {
// 调用无参构造函数
this();
// 添加集合中所有的元素
addAll(c);
}
内部类
这个内部类 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;
}
}
常用方法
添加操作
add(E)
链表尾部。具体添加到尾部的逻辑是由linkLast 方法完成的。
public boolean add(E e) {
添加到末尾
linkLast(e);
return true;
}
linkedLast() 方法
- 新添加元素 next 为 null,prev 为原 last 结点
- 新的 last = newNode
- 判断 last 是否为 null,为空则 newNode 是第一个元素 first
- 不为空则原 last 结点的 next 为 newNode
void linkLast(E e) {
// 临时节点 l 保存 last,就是 l 指向最后一个节点
final Node<E> l = last;
// 封装新的节点,值域为e,结点的前驱指向最后一个结点
final Node<E> newNode = new Node<>(l, e, null);
// 前面封装的结点成为了最后一个结点
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
eg:往链表中添加元素
List<Integer> list = new LinkedList<Integer>();
list.add(1);
list.add(2);
add(int,E) :linkBefore(element, node(index)),在非空结点前插入元素,和 linkFirst 原理一样
- newNode 的 prev 为 null,next 为原 first 结点
- 新的 first 结点为当前的 newNode
- 判断 first 是否空,为空则 last 结点为当前 newNode,即他是第一个结点
- 不为空,last 结点不变,原 first 的 prev 变为 newNode,next 不变。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
linkFirst 原理一样
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
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++;
}
删除操作
默认是从 first 结点开始删除数据
remove(Object o)
// 要移除的值在链表中存在多个一样的值,那么我们会移除index最小的那个,也就是最先找到的那个值,如果不存在这个值,那么什么也不做
public boolean remove(Object o) {
//这里可以看到,linkedList也能存储null
if (o == null) {
//循环遍历链表,直到找到null值,然后使用unlink移除该值。下面的这个else中也一样
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;
}
unlik() 方法
//不能传一个null值过,注意,看之前要注意之前的next、prev这些都是谁。
E unlink(Node<E> x) {
// assert x != null;
//拿到节点x的三个属性
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//这里开始往下就进行移除该元素之后的操作,也就是把指向哪个节点搞定。
if (prev == null) {
//说明移除的节点是头节点,则first头节点应该指向下一个节点
first = next;
} else {
//不是头节点,prev.next=next:有1、2、3,将1.next指向3
prev.next = next;
//然后解除x节点的前指向。
x.prev = null;
}
if (next == null) {
//说明移除的节点是尾节点
last = prev;
} else {
//不是尾节点,有1、2、3,将3.prev指向1. 然后将2.next=解除指向。
next.prev = prev;
x.next = null;
}
//x的前后指向都为null了,也把item为null,让gc回收它
x.item = null;
size--; //移除一个节点,size自减
modCount++;
return element; //由于一开始已经保存了x的值到element,所以返回。
}
查找
先校验 index 有效性,再将 index 与 size 中间值比较,若 index 较小,则从 first 开始往 last 放心遍历;如果 index 较大,则从 last 开始往 first 方向遍历。
**get(index) **
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// node()
//这里查询使用的是先从中间分一半查找
Node<E> node(int index) {
// assert isElementIndex(index);
//"<<":*2的几次方 “>>”:/2的几次方,例如:size<<1:size*2的1次方,
//这个if中就是查询前半部分
if (index < (size >> 1)) {//index<size/2
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;
}
}
indexOf(Object o)
//通过实体元素来查找到该元素在链表中的位置。跟remove中的代码类似,只是返回类型不一样。
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
修改操作
- 先校验 index 有效性
- node 方法返回当前 index 对应的 node (还是比较 index 与中值,在确定是从first 开始还是 last )
- 给原 node 赋予新的 element
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
总结
- linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构。
- 从源码中看,它不存在容量不足的情况,能存储 null 值
- 不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。