推荐阅读时间:20min+
目录:
-
回顾
- ArrayList中的JDK bug的由来以及Java中的逆变和协变
-
LinkedList
源码分析- 关键字
-
问题提出
- 为什么
ArrayList
和LinkedList
中很多的成员变量都是transient
的? -
LinkedList
如何同时实现栈和队列的功能? -
ArrayList
中的经典的CME异常会不会也在LinkedList
中重现?
- 为什么
源码分析
LinkedList源码分析
LinkedList的继承关系和文档中的关键字信息
如图可以明显的看出
LinkedList
主要实现了Queue
接口和Deque
(读作deck)接口。
通过阅读
LinkedList
文档和Deque
的文档可以通过如下的关键字获取到这一个数据结构的信息:
- two forms
- Doubly-linked list
- not synchronized / structural modification
总结一下LinkedList
和Deque
文档的大意:Deque
是一种支持从两端进行插入和删除的线性结构,Deque
被设计为可以作为队列(Queue)和栈(Stack),而且文档中特别注明了,如果需要使用和栈相关的操作,最好使用Deque
的实现,而不是legacy的Stack类
。LinkedList
是List
和Queue
的双链表实现,而且这个数据结构的所有操作都不是同步的,LinkedList
的两种迭代器iterator
和 listIterator
都是fail-fast的。
two forms
关键字中的two-form指的是insert
,remove
, examine
一个元素这些操作,这些操作都有两种实现,一种实现是当函数出现错误时抛出异常,另外一种实现再出现错误时会返回false
或者null
。
举例来说,
addFirst
和offerFirst
方法的区别一目了然:
void addFirst(E e);
boolean offerFirst(E e);
同时Deque
还列出了它实现的方法和Queue
接口中的方法还有作为Stack
的一种等效实现的方法的比较:
Doubly-linked list
双链表的结构如下:
LinkedList
的关键内部类Node
:
private static class Node {
E item;
Node next;
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
静态内部类嘛,懂得都懂,就是在实例化Node
的时候不会实例化一个外部类的引用,减小内存开销。
not synchronized / structural modification
说明链表时非同步的结构,但是和ArrayList
区别在哪里,下面的分析会详细谈到。
本篇源码分析除了让读者能够弄清楚代码之间的调用关系之外,还主要为了解决这样几个问题:
- 为什么
ArrayList
和LinkedList
中很多的成员变量都是transient
的? -
LinkedList
如何同时实现栈和队列的功能? -
ArrayList
中的经典的CME
异常会不会也在LinkedList
中重现?
下面会列出代码和函数的调用关系图,同时也会注重对比ArrayList
和LinkedList
之间的关系。
重要的常量
//初始化size为0
transient int size = 0;
//first和last初始化为null 因为没有内容
transient Node first;
transient Node last;
设置这些成员为transient
原因如下:
在序列化和反序列化过程中会反射调用readObject
和 writeObject
两种方法:
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);
// Write out all elements in the proper order.
for (Node x = first; x != null; x = x.next)
s.writeObject(x.item);
}
size
被写进了输出流,同时比LinkedList
这些节点本身更重要的是这些节点中的item信息。所以保存节点本身和节点和前后节点信息是没有必要的。
构造函数
LinkedList
有两种构造函数:
public LinkedList() {
}
public LinkedList(Collection extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection extends E> c) {
//将一个Collection的内容直接存放到这个链表结尾
return addAll(size, c);
}
public boolean addAll(int index, Collection extends E> c) {
// 越界检查
checkPositionIndex(index);
//在ArrayList中见过的操作
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//主要目的是为了找到前驱和后继节点
Node pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
//right-shift 连接
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node 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;
}
//通过一个二分的方法省去了一半的查找时间
Node node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
这个构造函数的调用过程如下。这里要注意的是LinkedList
的插入过程是一个right-shift
的,index
后面的内容移动到右边。
第二个
addAll(int index, Collection c)
修改了modCount
,这里图画错了。
增
add 系列
add
函数家族有很多函数,addAll
函数已经分析过了,它的重载函数的逻辑也没有什么特别的,直接看一下这个函数:
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 l = last;
final Node newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
void linkBefore(E e, Node succ) {
// assert succ != null;
final Node pred = succ.prev;
final Node newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
可以看出add
函数是基于linkLast
和linkBefore
两个函数的,这两个函数帮助链表连接节点,同时根据情况是否进行right-shift
操作,同时也进行modCount
的计算。
public void addFirst(E e) {
linkFirst(e);
}
同理addLast
也是调用linkLast()
这里不再赘述了。
但是因为LinkedList
还实现了队列和栈的功能,所以『增』相关的操作还包括offer
系列的函数和push
函数:
offer系列
public boolean offer(E e) {
return add(e);
}
offer
函数表现的是一个将一个元素放置在链表尾部的过程(即一个队列排队到尾部的操作,由于LinkedList
是一个双向链表,所以必然有两端的操作:
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
public void push(E e) {
addFirst(e);
}
push
操作也比较简单,这里要注意可能和大家想象的不一样的是新加入的节点放入的是链表的头部。
删
clear 函数
clear
函数,clear
函数除了要删除头尾节点让整个链表销毁之外更需要删除中间节点的相关属性,避免内存泄漏。
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
for (Node x = first; x != null; ) {
Node next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
remove 系列
remove
系列函数,这里直接看一下方法调用链上的核心remove(Object o)
方法和方法调用链上的『孤儿』removeLastOccurence(Object o)
方法
//实际上是删除第一个碰到的元素
public boolean remove(Object o) {
//LinkedList可以放入空元素
if (o == null) {
for (Node x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node x) {
// assert x != null;
final E element = x.item;
final Node next = x.next;
final Node prev = x.prev;
//是不是第一个元素
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//是不是最后一个元素
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
//模仿remove(Object o )过程倒序遍历删除
public boolean removeLastOccurrence(Object o) {
if (o == null) {
for (Node x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
poll系列
除了List
自带的remove
等等函数,LinkedList
还有poll
q系列函数:
public E poll() {
final Node f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E pollFirst() {
final Node f = first;
return (f == null) ? null : unlinkFirst(f);
}
果不其然
poll
和pollFirst
的代码是相同的,因为都是删除队列头节点。pollLast
就是使用了unlinkLast
而已,这里不列举了。
pop 函数
pop
函数。毕竟LinkedList
实现了Deque
(双端队列),而不是什么双端栈的操作,所以增加元素和删除元素的操作只在一侧进行。
public E pop() {
return removeFirst();
}
前面说过当使用LinkedList
实现栈的时候是将链表的头当做栈顶,这里removeFirst
很显然是这样。这些删除函数中poll
系列和pop
函数会返回节点,而remove
系列函数中有些返回了节点,有些是boolean
,具体得看文档。
遍历
-
peek
相关函数
peek
相关函数有peekFirst
,peekLast
,因为peekFirst
和peek
代码相同,而且peekFirst
和peekLast
代码逻辑相同,这里就以peek
函数为例:
public E peek() {
final Node f = first;
//返回第一个节点的内容
return (f == null) ? null : f.item;
}
- 迭代器
谈到线性数据结构的遍历当然要看一下链表中的迭代器的结构
Iterator
接口自不必说,ListIterator
接口除了继承了Iterator
接口的遍历功能之外,他的显著特点是可以从表结构的任意一个方向进行遍历。
由于DescendingIterator
中保留了一个ListItr
的实例,我们先看看ListItr
:
ListIterator
的代码本身并不复杂,而且全部列举出来意义不大 而且比较占篇幅,这里通过方法之间的调用情况来简单说明与一下。
ListIterator
中的lastReturned
变量主要用于说明最近一次变化的结果,举个例子:
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
next
操作就是取出链表中的下一个节点的位置,这里就回标记lastReturned
。同时previous
remove
这些操作都是基于lastReturned
的。
可以看到ListIterator
中凡是和结构性变化(structural modification
)有关的操作都通过expectedModCount
和modCount
的值保持一致。
DescendingIterator
DescendingIterator
就是对于ListItr
的一个封装,以便反向遍历链表,代码就这么多:
private class DescendingIterator implements Iterator {
private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}
LinkedList 中是否也会出现CME异常?
ArrayList arrayList = new ArrayList<>();
arrayList.add(1); arrayList.add(2);arrayList.add(3);
arrayList.add(4); arrayList.add(5);arrayList.add(6);
for (Integer i : arrayList) {
//java.util.ConcurrentModificationException
if(i ==6) arrayList.remove(i);
}
LinkedList linkedList = new LinkedList<>();
linkedList.add(1); linkedList.add(2);linkedList.add(3);
linkedList.add(4); linkedList.add(5);linkedList.add(6);
for (Integer i : linkedList) {
//没有异常
if(i ==6) linkedList.remove(i);
}
按照上一篇文章的内容中可以得出 第一段代码会出现CME,第二段代码只将ArrayList
改成为LinkedList
,不会出现异常的愿意是JDK有意为之吗?
当然不是,下面通过了一个图示分析这个过程:
//remove()方法通过unlink()改变modCount
public boolean remove(Object o) {
if (o == null) {
for (Node x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
// Java中的foreach语句其实就是一个迭代器的语法糖,使用迭代器时会调用next方法 同时通过 hasNext()检测是否结束
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
//LinkedList中的hasNext
public boolean hasNext() {
return nextIndex < size;
}
//ArrayList中的hasNext
public boolean hasNext() {
return cursor != size;
}
通过分析删除过程可以发现虽然删除了之后,LinkedList
持有的modCount
和缓存在ListIterator
中的expectedModCount
不相等,但是hasNext
会返回false
(size
为5 nextIndex
为6),如果是ArrayList
中的hasNext
,这个时候size
= 5 cursor
= 6,依然返回true,所以不可避免的进入下一轮迭代,并且checkForComodification()
出现CME。
当然在LinkedList
中不会出现CME的原因是被删除的元素在链表的结尾,checkForComodification
函数没有被执行,如果是这样的代码:
LinkedList linkedList = new LinkedList<>();
linkedList.add(1); linkedList.add(2);linkedList.add(3);
linkedList.add(4); linkedList.add(5);linkedList.add(6);
linkedList.add(7); linkedList.add(8);linkedList.add(9);
for (Integer i : linkedList) {
//error
if(i ==6) linkedList.remove(i);
}
还是会出现异常。
参考资料:
Iterator 和 ListIterator的区别
LinkedList 和 ArrayList 对比问题
JDK9修复的bug(JDK-6260652)是怎么回事