目录
1.LinkedList
1.1整体架构
- 底层使用了双向链表来进行实现,有一个头节点,尾节点和一个元素个数size
- 这里的头节点和尾节点不是哨兵的概念,只是单纯的指针指向概念!!!
public class LinkedList<E>
{
//元素个数
transient int size = 0;//transient 表示不可序列化字段
//头节点
transient Node<E> first;
//尾节点
transient Node<E> last;
}
- 每个Node有prev和next指针以及val节点值
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;
}
}
1.2追加源码分析
- 尾部追加linkLast
// 从尾部开始追加节点
void linkLast(E e) {
// 把尾节点数据暂存
final Node<E> l = last;
// 新建新的节点,初始化入参含义:
// l 是新节点的前一个节点,当前值是尾节点值
// e 表示当前新增节点,并且当前新增节点后一个节点是 null
final Node<E> newNode = new Node<>(l, e, null);
// 更新尾节点指针指向
last = newNode;
// 判断链表是否为空
if (l == null) // 通过头节点也可以进行判断
// 更新头节点指针指向
first = newNode;
// 否则,把前尾节点的下一个节点,指向当前尾节点。
else
l.next = newNode;
// 更新size大小和版本号
size++;
modCount++;
}
首先预存尾节点的指针作为旧的尾节点,在新建一个节点,并且设置该节点的prev和next指针
在更新尾节点,在判断链表是否为空,空链表需要更新头节点,不为空就需要更新旧的尾节点的next指针
最后需要更新size的大小和版本号的值
- 头部追加linkFirst
// 从头部追加
private void linkFirst(E e) {
// 头节点赋值给临时变量
final Node<E> f = first;
// 新建节点,前一个节点指向null,e 是新建节点,f 是新建节点的下一个节点,目前值是头节点的值
final Node<E> newNode = new Node<>(null, e, f);
// 更新头节点的指针指向
first = newNode;
// 判断链表是否为空
if (f == null)
// 更新尾节点的指针指向
last = newNode;
// 旧的头节点的前继指针指向新的头节点
else
f.prev = newNode;
// 更新链表长度和版本号
size++;
modCount++;
}
头部追加和尾部追加,基本是相同的方法。
预存头节点的指针,新建节点,更新头节点的指针指向,判断链表是否为空(为空需要更新尾节点指针指向,不为空需要更新旧的头节点的前驱指针),最后更新链表的长度和版本号
1.3节点删除
跟追加类似,也可以头部删除和尾部删除,这里只分析头部删除
//从头删除节点 断言:f 是链表头节点
//一般配合removeFirst方法来使用
private E unlinkFirst(Node<E> f) {
// 拿出头节点的值,作为方法的返回值
final E element = f.item;
// 拿出头节点的下一个节点
final Node<E> next = f.next;
//帮助 GC 回收头节点
f.item = null;
f.next = null;
// 更新头节点的指针指向为next节点
first = next;
//如果 next 为空,表明链表为空
if (next == null)
last = null;
//链表不为空,头节点的前一个节点指向 null
else
next.prev = null;
//修改链表大小和版本
size--;
modCount++;
return element;
}
public E removeFirst() {
final Node<E> f = first;
//断言头节点
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
unlinkFirst方法一般配合removeFirst方法来进行使用
先获取要删除节点的值用于返回,在获取到头节点的next节点,在设置头节点的值和next指针为null帮助GC回收(前驱节点一定为null),
在更新头节点的指针指向为next,在判断删除后链表是否为空(为空需要更新尾节点,不为空需要更新next节点的前驱指针为null),最后更新size大小和版本号;
1.4节点查询
// 根据链表索引位置查询节点
// 一般配合get方法进行使用
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 {// 如果 index 处于队列的后半部分,从尾开始找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
public E get(int index) {
// 检验index是否安全
checkElementIndex(index);
return node(index).item;
}
node方法一般是配合get方法来进行使用的
利用了双向链表和属性封装的特点,耗时可以减少一半
如果index索引位置在前半区,就从头开始找,如果index索引位置在后半区,就从尾部开始找
ps:注意是索引位置,从0开始
2.迭代器ListIterator
2.1整体架构
- 使用Iterator只能实现单向遍历和三个方法,比较单一,在Iterator类的基础上增加了双向遍历
// 双向迭代器
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;// 当前指向的节点
private Node<E> next;// 下一个节点
private int nextIndex;// 下一个节点的位置
private int expectedModCount = modCount;// 期望版本号
ListItr(int index) {
// 会初始化next节点
// 比如index=7,会找到第八个元素,但是只会取到前七个元素,需要注意
next = (index == size) ? null : node(index); //根据索引位置查找
nextIndex = index;
}
}
ps:要注意新维护了一个nextIndex来指代next的意义
ps:比如index8,会找到第八个元素,但是只会取到前七个元素,需要注意
2.2三个方法源码解析
// 在这里nextIndex代表的是节点的个数
public boolean hasPrevious() {
return nextIndex > 0;
}
// 取前一个节点
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// next 为空场景:1:说明是第一次迭代,取尾节点(last);2:上一次操作把尾节点删除掉了
// next 不为空场景:说明已经发生过迭代了,直接取前一个节点即可(next.prev)
lastReturned = next = (next == null) ? last : next.prev; //在这里next和lastReturned一直一样
// 更新索引位置
nextIndex--;
return lastReturned.item;
}
首先检查版本号,在判断是否有前继节点可以迭代,在判断是否是第一次迭代(是第一次迭代那么next节点为null,不是第一次向前遍历即可),
然后更新索引的位置,最后返回当前指针节点的元素
ps:nextIndex在这里代表的是节点的数量,
ps:next和lastReturned的值会一直一样,在这里下一个节点和当前节点指向同一个位置
// 在这里nextIndex代表的是索引位置
public boolean hasNext() {
return nextIndex < size;// 下一个节点的索引小于链表的大小,就有
}
// 取下一个元素
public E next() {
//检查期望版本号有无发生变化
checkForComodification();
//进行检查
if (!hasNext())
throw new NoSuchElementException();
// 第一次执行时,是在初始化迭代器的时候,next 是被赋值的
lastReturned = next; //返回当前指针位置的元素,next和lastReturned始终差一个
// 更新next节点,为下次迭代做准备
next = next.next;
// 更新索引位置
nextIndex++;
return lastReturned.item;
}
方法流程大致与previous相同:检验版本号,在判断是否可以迭代,在更新当前元素的指针和下一个元素的指针
ps:在这里nextIndex代表的值为下一个节点的索引位置,默认为0
ps:next和lastReturned每次差一个元素,next指代下一个元素,lastReturned指代当前元素
方法小结:
- 初始化方法应传入的是节点数量,而不是索引值的大小,比如7,对于previous方法会索引到前7个元素,next方法会索引到剩余size-7个元素;
- previous方法的当前节点元素和前一个节点元素相同,next的当前节点元素和前一个节点位置总是差一;
- previus方法停止的判断是nextIndex==0的时候,next方法停止的判断是nextIndext==size的时候;
public void remove() {
checkForComodification();
// lastReturned 为空,说明调用者没有主动执行过 next() , previos() 或者 重复删除
// lastReturned 不为空,是在上次执行 next() 或者 previos()方法时赋的值
if (lastReturned == null)
throw new IllegalStateException();
//反向遍历的时候需要用到
Node<E> lastNext = lastReturned.next;
//删除当前节点
unlink(lastReturned);
// 从尾到头遍历,next == lastReturned始终会成立,next不能为null的因为还需要遍历
if (next == lastReturned)
next = lastNext;
else //从头到尾遍历
nextIndex--;
//防止重复删除,当前指针元素置为null
lastReturned = null;
//更新版本号
expectedModCount++;
}
先进行版本号检查,在判断是否重复删除当前元素,在获取当前元素的next值(反向遍历时,要给next赋值否则无法继续遍历),在进行删除当前节点,
在判断是否是反向遍历(是反向遍历就要给next赋值防止无法遍历,nextIndex表示的是元素的数量,因为当前元素是已经遍历过的,所以不需要在减,是正向遍历next值不影响,nextIndex表示的是元素的索引位置需要减一)
在就是为了防止重复删除,并且标识当前元素已经删除会设置为null,最后更新版本号