jdk1.7 LinkedList源码分析
今天手头比较闲,抓紧时间来整理一下1.7LinkedList源码,加深对链表的理解,有不足的地方请大家指正。
LinkedList概述
开篇先总结一下 LinkedList是一个双向链表的数据结构,他不光实现了List接口,同时也实现了Deque双向队列接口。所以LinkedList不但有操作List的方法,也有队列相关的操作方法。值得注意的是LinkedList是一个线程非安全的类,因此在多线程环境下操作LinkedList的时候要注意同步加锁,在jdk中提供的Collections的synchornizedList(List list)方法使LinkedList变成一个可以线程安全的类。LinkedList也实现了Iteator接口,他生成的iteartor和listIterator也是fail-fast,就是在迭代过程不允许通过LinkedList本身的增删改方法去改变这个链表,如果有这样的操作迭代的过程就会抛出ConcurrentModificationException异常,只能在迭代的过程通迭代器本身的增删方法去操作LinkedList链表。以前有同事说既然fail-fast机制那么在多线程环境下去操作LinkedList就可以保证线程安全,这个想法是错误的,官方文档明确指出fail-fast快速失败行为是不能得到保证的,这个机制只是最大努力的抛出ConcurrentModificationException异常,因为在多线程环境下,可能在一个线程中修改了LinkedList但是还没来得及更新modCount,另外一个线程发现modCount还是原来的值这个时候就不会抛出ConcurrentModificationException
所以fail-fast他只是用来检查程序错误。下图提供了LinkedList继承机构图
LinkedList双向链表的实现和成员变量
LinkedList是一个双向链表但是他又实现了Deque双向队列接口,首先了解一下双向链表的实现。这里我粘出LinkedList结点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;
}
}
从这段代码可以看出LinkedList中的每个结点内包含了当前结点数据,和上一个节点的引用已经下一个结点的引用,多个节点按照这种结构结合就形成了一个双向链表结构。这个结构也就是我们LinkedList的数据结构类型。
LinkedList的成员变量
//LinkedList的结点个数
transient int size = 0;
//LinkedList的头结点
transient Node<E> first;
//LinkedList的尾部结点
transient Node<E> last;
为什么LinkedList设计的时候要专门保持头结点和尾部结点呢?这还是得从LinkedList的数据结构来解释,因为不同于ArrayList的可变数组数据结构,LinkedList他是一个双向的链表是内存不连续的,所以我们去寻找链表的结点就得从节点的头部或者尾部进行遍历依次寻找。这个时候LinkedList的头尾结点就是我们去寻找的入口,如果我们通过指定的索引index去寻找结点,可以先判断index和size/2的大小来决定是从头部开始遍历还是从尾部开始遍历,这样可以节约查询时间。从这几点来看可以发现和ArrayList比较起来LinkedList的查询效率要低很多,ArrayList可以通过index直接定位到元素,LinkedList得从头部或者尾部遍历寻找。
LinkedList的构造函数
LinkedList有两个构造函数
//无参构造函数 生成一个空链表 first=last=null
public LinkedList() {
}
/**
* 传入一个集合参数,构造列表的时候是先获取结合的元素 然后按照链表的结构进行构造
*
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(int index, Collection<? extends E> c) {
//判断索引是不是合法,不合法直接抛出异常
checkPositionIndex(index);
//将集合元素转为数组
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
//如何index==size就是将集合元素追加到链表末尾
if (index == size) {
succ = null;
pred = last;
} else {
//如果index!=size就是将LinkedList链表在index位置拆开然后追加集合的元素
succ = node(index);
pred = succ.prev;
}
/*
构造链表结构 遍历集合元素 为每一个元素封装成一个新结点,构造链表方式又分好几种情况
a:当初始链表为空链表情况也就是first=last=null 这个时候就将新结点赋值给first结点
然后后续遍历集合构造的新结点通过pre属性和next属性依次关联上去形成一个双向链表
b:当初始链表不是空链表也就是first 和last 结点都不是null 这个时候遍历集合构造
新结点 通过pre和next属性关联在初始链表的last结点之后
c:当初始链表不为空,且index!=0&&index!=size 时候 就需要先获取 index位置的结点 然后将初始链表
从这个位置拆开,遍历集合构造新结点就是从index位置开始关联,最后末尾将原先拆开后的
另外一部分在重新关联到新链表的末尾
*/
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//遍历数组 为每个数组元素构造新的结点 且新的结点指向的pre是初始链表被追加的位置
//结点
Node<E> newNode = new Node<>(pred, e, null);
//如果pre为null说明初始链表为是一个空链表 first=last=null
if (pred == null)
//将新结点当做是初始结点
first = newNode;
else
//pred!=null就将pre结点的next指向新结点 这样的话就在pre结点和newNode结点之间形成了一个双向的链表结构
pred.next = newNode;
pred = newNode;
}
//succ==null是在index==size或者index==0的情况下 在这个情况下新结点只需在追加在
//初始结点末尾,最后将链表的last赋值为pre即可。其中succ保存的是index位置之后的链表
if (succ == null) {
last = pred;
} else {
//这个情况就是index!=0 index!=size 也就是新结点追加位置是在初始链表index位置开始
//追加,最后在合并上succ指向的另外一部分链表
pred.next = succ;
succ.prev = pred;
}
//最后更改size长度
size += numNew;
modCount++;
return true;
}
经过上面的代码分析了两种构造函数构造链表的处理方式,无参构造函数构造一个first=last=null的空链表。而带有集合参数的构造函数构造链表实际上是调用的addAll(int index, Collection<? extends E> c)方法进行批量结点增加。对于批量结点增加主要是通过以下几步:
1.首先index参数的值要进行校验 他不能小于0或者大于size 否则会抛出异常,在构造函数这里这个index实际上就是0;
2.通过集合的toarray方法将集合元素转换为一个数组,然后遍历数组;
3.然后将数组每个元素构造成新结点new Node<>(pred, e, null),其中pre很重要,通过指向的是需要追加的位置,如果index=0或者index=size pred追加的气势就是 初始链表的last结点,如果index!=0 &&index!=size pred追加的位置就是index-1位置的结点。
LinkedList的增删该查(先将List接口后面再所Deque接口)
LinkedList实现的List接口的增加方法,分开来讲调理更清晰一些
add(E e)
add(int index, E element)
addAll(Collection<? extends E> c)
addAll(int index, Collection<? extends E> c)
/**
*添加元素 实际上调用的是LinkLast()方法 在末尾追加新结点
*/
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
//构造新结点
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
//l==null说明初始链表是一个空链表first=last=null 将newNode赋值给first和last结点
if (l == null)
first = newNode;
else
//将原初始链表的last结点next指向新结点 这个时候新结点就变成原始链表中的last结点了
l.next = newNode;
size++;
modCount++;
}
这个add方法比较简单 就是在链表末尾追加新结点 如果是空链表 就把新结点当做是原始链表的first结点和last结点
//在指定所以位置插入新结点
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//在链表末尾插入新结点
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//返回指定位置的非nul结点
Node<E> node(int index) {
// assert isElementIndex(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;
}
//在指定位置结点前插入新结点
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;
//如果pred==null只有在index=0的时候 这个时候succ就是first结点
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
add(int index, E element)这个方法在指定位置插入新结点,具体的实现是分两步走,当index==size 那么久调用 linkLast(E e) 在链表末尾添加元素,当index!=size的时候就调用 linkBefore(element, node(index) 在指定结点处添加元素,其中获取指定位置的结点方法是node(int index) 大家在这个方法的注释可以发现他返回的是一个非null结点元素,为什么可以断言他返回一个非null结点元素呢?这时因为返回null结点只有在index为0的情况下且这个初始化链表是first=last=null
但是在index=0 且size=0的情况下我们的方法走的是linkLast(E e),不会走到 node(int index)这个方法中,所以可以断言这个方法返回的是一个非null元素结点。大家可以发现在node(int index)方法中 遍历的时候f(index < (size >> 1))先判断index和size/2的位置来决定从头或者从尾开始遍历这样的好处就是加快了遍历速度。
addAll这两个方法在构造函数时候已经分析过就不在重复分析
LinkedList删除结点的方法
remove()
remove(Object o)
remove(int index)
//移除链表的头部结点
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
//获取头结点next指向的下一个结点
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
//把下一个结点赋值给first结点
first = next;
//如果next为空说明初始链表就是只有一个结点的链表,移除first结点后初始链表应该变成为first=last=null状态
if (next == null)
last = null;
else
//因为next结点原来指向的是first结点 现在first结点被移除 next结点变成first结点就要将prev指向为null,这样才是一个新的first结点
next.prev = null;
size--;
modCount++;
return element;
}
remove()方法调用的是removeFirst()见名知意 就是移除链表的第一个元素结点也就是first结点。如果当链表是一个空链表first=last=null情况方法直接就会抛异常报错。当链表不是空链表的时候就去调用unlinkFirst(Node f) 。在这个方法中要注意的是当我们的初始链表只有一个结点这个时候first=last!=null,当我们要移除first结点这个时候链表应该是一个空链表的状态,所以要将last结点也赋值为null这样链表就满足空链表状态first=last=null
public boolean remove(Object o) {
//当要移除的结点包含的元素是null的时候
if (o == null) {
//遍历链表 从头结点开始遍历 当找到结点包含元素为null时候调用unlink(x)
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
//当要移除的结点包含的元素不是null的时候
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) {
// assert x != null;
final E element = x.item;
//获取到这个即将要被移除结点的前一个结点 和 后一个结点
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果前一个结点为null 这说明移除结点是first结点
//这个时候我们只要将移除结点的next结点赋值为first结点即可
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//如果后一个结点为null 这说明移除结点是last结点
//这个时候我们只要将移除结点的prev结点赋值为last结点即可
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
分析完unlink(Node x)方法之后我们在看remove(int index)
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
在上文中已经讲过node(int index)方法 这个方法实在寻找指定位置的结点
然后在调用unlink(Node x)方法 实现方式和remove(Object o) 是一样的
LinkedList查询结点的方法
get(int index)
indexOf(Object o)
lastIndexOf(Object o)
public E get(int index) {
//判断index值是否在[0,size)这个区间不在就抛出异常
checkElementIndex(index);
//通过node方法查询结点在获取结点包含的元素
return node(index).item;
}
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;
}
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
从代码的实现来看 我们可以发现LinkedList链表的查询效率和ArrayList比起来就要差很多 LinkedList必须得从头或者从尾部依次去寻找 ,ArrayList可以直接通过索引定位到元素,这就是连续的内存空间和非连续内存空间的区别之一
LinkedList修改结点的方法
set(int index, E element)
public E set(int index, E element) {
//判断index的区间[0,size) 不在就抛出异常
checkElementIndex(index);
//获取到指定位置的结点
Node<E> x = node(index);
//替换新元素 方法最后返回被替换的元素
E oldVal = x.item;
x.item = element;
return oldVal;
}
以上分析的是LinkedLlist链表实现List接口的增删改查方法,下面将分析LinkedList实现的Deque双向队列的增删该查方法。
LinkedList作为Deque的增删改查
Deque实现的是Queue接口 在Queue接口定义的规范是一个FIFO先进先出的队列,所以Deque可以当做是一个FIFO队列,但是Deque接口本身定义了部分方法满足LIFO规则,也就是说Deque这个双端队列即可当做栈(LIFO),也可当作是队列(FIFO),而LinkedList实现了Deque接口所以 LinkedList链表既可以当做栈使用 又可以当做队列来用。
首先我们来看下Queue接口定义的方法
public interface Queue<E> extends Collection<E> {
/**
* Inserts the specified element into this queue if it is possible to do so
* immediately without violating capacity restrictions, returning
* <tt>true</tt> upon success and throwing an <tt>IllegalStateException</tt>
* if no space is currently available.
*
* @param e the element to add
* @return <tt>true</tt> (as specified by {@link Collection#add})
* @throws IllegalStateException if the element cannot be added at this
* time due to capacity restrictions
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this queue
* @throws NullPointerException if the specified element is null and
* this queue does not permit null elements
* @throws IllegalArgumentException if some property of this element
* prevents it from being added to this queue
*注释的意思是将指定元素插入队列(如果立即可行且不违反容量限制),在成功是返回true,如果没有可用空间抛出异常
*/
boolean add(E e);
/**
* Inserts the specified element into this queue if it is possible to do
* so immediately without violating capacity restrictions.
* When using a capacity-restricted queue, this method is generally
* preferable to {@link #add}, which can fail to insert an element only
* by throwing an exception.
*
* @param e the element to add
* @return <tt>true</tt> if the element was added to this queue, else
* <tt>false</tt>
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this queue
* @throws NullPointerException if the specified element is null and
* this queue does not permit null elements
* @throws IllegalArgumentException if some property of this element
* prevents it from being added to this queue
* 注释的意思是将指定元素插入队列(如果立即可行且不违反容量限制),当使用容量限制队列是,此方法优于add(E),后者可能无法插入元素抛出异常
*/
boolean offer(E e);
//获取并移除此队列的头
E remove();
//获取并移除此队列的头,如果队列为空,则返回null
E poll();
//获取但不移除此队列的头
E element();
//获取但不移除此队列的头,如果此队列为空则返回null
E peek();
}
这几个方法在LinkedList中都有实现
add(E e)
offer(E e)
//往队列中插入元素这个在前面分析过,插入元素是添加在队里末尾
public boolean add(E e) {
linkLast(e);
return true;
}
//offer方法的实现和add方法是一样的
public boolean offer(E e) {
return add(e);
}
在LinkedList中实现的Queue接口的add 和offer方法纯粹就是在链表末尾插入新的元素 ,没有空间限制这个因素在里面。所以我个人觉得这个实现其实是有的牵强的。不过在LinkedBlokeQueue实现的Queue接口里面是有空间限制这个概念的。
//获取头结点 如果链表是空链表(空队列) 那么就返回null
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取头结点 如果链表是空链表(空队列) 那么就返回null,同时移除链表中的头结点
public E poll() {
final Node<E> f = first;
//unLinkFirst方法在上文中已经分析了
return (f == null) ? null : unlinkFirst(f);
}`
//获取头结点
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
以上这几个方法就可以是先队列的FIFO操作
我们在来看一下Deque接口定义的一些方法
public interface Deque<E> extends Queue<E> {
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getFirst();
E getLast();
E peekFirst();
E peekLast();
boolean removeFirstOccurrence(Object o);
boolean removeLastOccurrence(Object o);
// *** Queue methods ***
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
// *** Stack methods ***
void push(E e);
E pop();
// *** Collection methods ***
}
在LinkedList实现的Dqueue接口方法
//往链表头部插入新结点
public void addFirst(E e) {
linkFirst(e);
}
//往链表尾部插入新结点
public void addLast(E e) {
linkLast(e);
}
//往链表头部插入新结点
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
//往链表尾部插入新结点
public boolean offerLast(E e) {
addLast(e);
return true;
}
//获取链表头部结点 如果链表为空链表 返回null
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取链表尾部结点 如果链表为空 返回null
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
//获取并移除头部结点 如果链表为空返回null
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//获取并移除链表尾部结点 如果链表为空 返回null
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
//往链表头部加入新结点 这个方法就是stack栈操作的压栈
public void push(E e) {
addFirst(e);
}
//获取并移除链表的头结点 这个方法是stack栈操作的出栈
public E pop() {
return removeFirst();
}
现在我们来比较一下在LinkedList中Deque实现的队列和Queue实现的队列的异同
//往链表尾部插入新结点(Deque)
public void addLast(E e)
(E e) {
linkLast(e);
}
//往链表尾部插入新结点(Deque)
public boolean offerLast(E e) {
addLast(e);
return true;
}
//往链表尾部插入新结点(Queue)
public boolean add(E e) {
linkLast(e);
return true;
}
//offer方法的实现和add方法是一样的(Queue)
public boolean offer(E e) {
return add(e);
}
//获取链表头部结点 如果链表为空链表 返回null(Deque)
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取头结点 如果链表是空链表(空队列) 那么就返回null(Queue)
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取并移除头部结点 如果链表为空返回null(Deque)
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//获取头结点 如果链表是空链表(空队列) 那么就返回null,同时移除链表中的头结点(Queue)
public E poll() {
final Node<E> f = first;
//unLinkFirst方法在上文中已经分析了
return (f == null) ? null : unlinkFirst(f);
}`
因为队列是先进先出(FIFO),所以存放元素的时候就放在链表尾部在应用的时候我们可以调用addLast,offerLast ,add ,offer 这四个方法去存放元素 然后调用 peekFirst ,pollFirst,peek,poll 去获取元素 这样的一存一拿刚好就符合了FIFO 原则 形成了一个队列
因为堆栈的定义是在双端队列Deque中定义的 所以我们分析一下Deque中设计出堆栈特性的方法
//往链表头部加入新结点 这个方法就是stack栈操作的压栈
public void push(E e) {
addFirst(e);
}
//获取并移除链表的头结点 这个方法是stack栈操作的出栈
public E pop() {
return removeFirst();
}
看他内部调用的方法,已经显而易见他的实现原理
以上LinkedList实现的Deque接口的方法 在上文中都已经分析过了,也就是如果不需要实现这个接口Deque LinkedList本身的一些链表操作其实也可以模拟出堆栈和队列的特性操作。其实大致分析下来 我们就能发现LinkedList这个类本身方法就是那么几个,大部分的方法都是互相调用的,jdk开发人员全可以交由应用开发人员自己去调用组合这些方法。但是为了更好的封装性 以及对外提供的api更友好,在LinkedList中把这些方法按照需要的队列或这是堆栈的特性,重新组合封装 提供给应用开发人员更加简单和友好的api。个人理解~~~~~~~~~~~~~
这就是我写这篇博文的时候为什么先单独分析LinkedList作为一个双向链表的增删改查原理,因为后面的实现的Deque双端队列和Queue队列接口方法的时候其实调用的就是List的增删改查方法组合。
LinkedList的迭代器
对于集合来说遍历集合是每个集合容器都需要支持的功能,对于LinkedList的遍历我们可以使用普通的for循环,高级for循环,这里我介绍另外一种遍历方式:通过迭代器进行遍历。什么是迭代器呢?其实迭代器也是一个设计模式,他是为了隐藏容器的内部具体实现,调用者不需要关心容器内部到底是什么数据结构,只需要知道容器中还有没有元素。这样做有什么好处呢?比如说当我们方法中传入一个集合对象,可能是ArrrayList,LinkedList 或者是HashSet等等,如果我们要用for循环去遍历他们 那就要针对每种集合数据类型 分开写循环代码,但是如果使用迭代器 那么我们就不用管集合的数据类型,使用相同的迭代器代码就可以实现遍历不同集合的元素。话不多说先上代码
LinkedList没有单独的Itearator实现类,只有ListItreator实现类(LinkedList内部类ListItr) 不同于Arraylist 在其类部类中都有Iiteartor和ListIterator的实现类。但是从LinkedList继承体系来看LinkedList实现了Iterator接口 必然有要实现Iterator iterator()这个方法,其实这个方法是在LinkedList父类中实现的 但是父类的实现返回的也是ListIterator,所以说LinkedList只有一个迭代器就是ListItreator
private class ListItr implements ListIterator<E> {
//上一个结点
private Node<E> lastReturned = null;
//下一个结点
private Node<E> next;
//下一个索引位置
private int nextIndex;
//当前LinkedList的修改次数
private int expectedModCount = modCount;
//构造方法指定了迭代初始索引值
ListItr(int index) {
// assert isPositionIndex(index);
//如果设置初始的遍历索引位置刚好是size 说明已经遍历到尾部了,那么下一个遍历的结点应该为null,如果index位置不等于size 获取到下一个结点赋值给next
next = (index == size) ? null : node(index);
nextIndex = index;
}
//判断往尾部方向是否还有结点可以寻找
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
//判断迭代器的expectedModCount和容器本身的modCount是否一致 不一致抛出异常这就是fail-fast
checkForComodification();
//判断是否还有结点 没有也抛出异常
if (!hasNext())
throw new NoSuchElementException();
//我们在迭代器初始化的时候给定了一个迭代初始位置的索引,并获取到索引位置的结点
//赋值给next 这是跌代器的初始状态,当调用者调用next()方法后,就把初始化状态的
//next值赋给lastReturned,代表上次的迭代返回的结果,本次迭代的结果就复制给next
//同时索引位置自增
lastReturned = next;
next = next.next;
nextIndex++;
//把上次迭代的结果返回给调用者
return lastReturned.item;
}
//判断往头部方向遍历是否还有元素
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
//判断迭代器的expectedModCount和容器本身的modCount是否一致 不一致抛出异常这就是fail-fast
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
//将next结点的上一个元素赋值给lastReturned
lastReturned = next = (next == null) ? last : next.prev;
//索引自减
nextIndex--;
//将上一个结点返回给调用者
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
//在我们迭代的过程中如果容器进行了增删该那么modeCount就会改变,然后和迭代器初始
//化时候的expectedModCount不同,这个时候迭代器checkForComodification()就会抛出异常,但是当我们在迭代器内部
//调用remove方法的时候,会对expectedModCount也会自增,也会对modCount自增,最终expectedModCount和容器本身的modCount是一样的
//所以在迭代器迭代的时候调用迭代器的remove不会抛异常
public void remove() {
//判断迭代器的expectedModCount和容器本身的modCount是否一致 不一致抛出异常
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
//调用集合本身的方法去删除结点
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
//修改结点包含的元素
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
//添加新的结点 添加的过程也需要先判断容器的modcount是否和 expectedModCount一致
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
//AbstractSequentialList 通过父类可以发现iterator返回的也是ListIterator
//所以说LinkedList的迭代器其实就是一种ListIterator和ArrayList不同
public Iterator<E> iterator() {
return listIterator();
}```
小结
本文简单分析了一下LinkedList的增删改查操作,在此基础上简单叙述了LinkedList作为队列和栈的实现方式。附带说一点LinkedList实现了序列化接口
但是类本身的属性都是用transient关键字进行修饰的,意思就是序列化的时候这几个属性都不用写入二进制流,这样应该是为了加快序列化速度,减少序列化所占用的空间,这样反序列化的时候速度也加快了。
这里附上序列化代码
//当LinkedList序列化的时候会调用这段代码,从代码可以看出序列化的时候只是将
//链表的有效数据写入二进制流 ,不写没用的东西
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out size
s.writeInt(size);
//
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}
/**
* Reconstitutes this {@code LinkedList} instance from a stream
* (that is, deserializes it).
*/
@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
}