java中的数据结构源码解析的系列文章:
ArrayList源码分析
LinkedList与Queue源码分析java
上篇已经分析了基于数组实现数据存储的ArrayList(线性表),而本篇的主角是LinkedList,这个使用了链表实现数据存储的集合,它的增、删、查、改方式又会是怎样的呢?下面就开始对LinkedList的源码进行分析吧。node
在分析LinkedList以前,仍是先瞄一眼List接口,虽然前篇已经看过一遍了,但为了明确下文的分析方向,仍是先把List接口中的几个增删改查方法再列一次。数组
public interface List<E> extends Collection<E> {
boolean add(E e);
void add(int index, E element);
boolean remove(Object o);
E remove(int index);
E set(int index, E element);
E get(int index);
...
}复制代码
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;
...
}复制代码
LinkedList的成员变量不多,就上面那3个,其中first和last都是Node类型(即节点类型),用来表示链表的头和尾,这跟ArrayList就存在着本质的区别了。bash
要注意:
first和last仅仅只是节点而已,跟数据元素没有关系,能够认为就是2个额外的"指针",分别指着链表的头和尾。
数据结构
public LinkedList() {
}复制代码
LinkedList的构造函数有2个,以平时最经常使用的构造函数为例,发现该构造函数什么事都没作。函数
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是双向链表。
源码分析
为何Node这个类是静态的?答案是:这跟内存泄露有关,Node类是在LinkedList类中的,也就是一个内部类,若不使用static修饰,那么Node就是一个普通的内部类,在java中,一个普通内部类在实例化以后,默认会持有外部类的引用,这就有可能形成内存泄露。但使用static修饰过的内部类(称为静态内部类),就不会有这种问题,在Android中,有不少这样的状况,如Handler的使用。好像扯远了~post
好了,那下面就看看LinkedList是怎么进行增、删、改、查的。优化
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;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}复制代码
由于LinkedList是链表结构,因此每添加一个元素就是让这个元素连接到链表的尾部。
add(E e)的核心是linkLast()方法,它对元素进行了真正添加操做,分为如下几个步骤:ui
经过对add(E e)方法的分析,咱们也知道了,原来LinkedList中的元素就是一个个的节点(Node),而真正的数据则存放在Node之中(数据被Node的item所引用)。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}复制代码
该add方法将添加集合元素分为2种状况,一种是在集合尾部添加,另外一种是在集合中间或头部添加,由于第一种状况也是调用linkLast()方法,这里再也不啰嗦,咱们看看第二种状况,分析linkBefore(E e, Node succ)这个方法是怎么对元素进行添加操做的。
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++;
}复制代码
往LinkedList集合中间或头部添加元素分为如下几个步骤:
对于链表的操做仍是有些复杂的,特别是这种双向链表,不过仔细理解下,也不是什么问题(看不懂的能够边看步骤边动手画一画)。到这里,对于LinkedList的第一个添加方法就分析完了。
这也是LinkedList获取元素的核心方法,至关重要,由于后面会出现不少次,这里就顺带先分析一下了。
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;
}
}复制代码
细看node(int index)方法中的代码逻辑,能够看到,它是经过遍历的方式,将集合中的元素一个个拿出来,再经过该元素的prev或next拿到下一个遍历的元素,通过index次循环后,最终才拿到了index对应的元素。
跟ArrayList相比,由于ArrayList底层是数组实现,拥有下标这个特性,在获取元素时,不须要对集合进行遍历,因此查找某个元素会特别快(在数据量特别多的状况下,ArrayList和LinkedList在效率上的差异就至关明显了)。
不过,LinkedList对元素的获取仍是作了必定优化的,它对index与集合长度的一半作比较,来肯定是在集合的前半段仍是后半段进行查找。
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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;
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(int index)这个方法中,先经过index和node(int index)拿到了要被删除的元素x,而后调用了unlink(Node x)方法将其删除,天然,LinkedList删除元素的核心方法就是unlink(Node x),删除操做分如下几个步骤:
public boolean remove(Object o) {
if (o == null) {
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;
}复制代码
remove(Object o)这个删除元素的方法的形参o是数据自己,而不是LinkedList集合中的元素(节点),因此须要先经过节点遍历的方式,找到o数据对应的元素,而后再调用unlink(Node x)方法将其删除,关于unlink(Node x)的分析在第一个删除方法中已经提到了,可往回再看看。
LinkedList集合对数据的获取与修改均经过node(int index)方法来执行日后的操做,关于node(int index)方法的分析也已经在第一个添加方法的时候已经提过,这里也就再也不啰嗦了。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}复制代码
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}复制代码
这里要顺带分析下java中的队列实现,why?由于java中队列的实现就是LinkedList,你可能会疑问,队列的英文是Queue,在java中也有对应的接口,怎么会跟LinkedList扯上关系呢?由于LinkedList实现了队列:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
...
}复制代码
代码中的Deque是Queue的一个子接口,它继承了Queue:
public interface Deque<E> extends Queue<E> {...}复制代码
从这二者的关系,不可贵出,队列的实现方式也是链表。下面先来看看Queue的接口声明:
咱们知道,队列是先进先出的,添加元素只能从队尾添加,删除元素只能从队头删除,Queue中的方法就体现了这种特性。
public interface Queue<E> extends Collection<E> {
boolean offer(E e);
E poll();
E peek();
...
}复制代码
从上面这几个方法出发,来看看LinkedList是如何实现的。
public boolean offer(E e) {
return add(e);
}复制代码
能够看到,在LinkedList中,队列的offer(E e)方法其实是调用了LinkedList的add(E e),add(E e)已经在最前面分析过了,就是在链表的尾部添加一个元素~
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}复制代码
poll()方法先拿到队头元素 f ,若 f 不为null,就调用unlinkFirst(Node f)其删除。unlinkFirst(Node f)在实现上跟unlink(Node x)差很少且相对简单,这里不作过多说明。
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}复制代码
peek()先经过first拿到队头元素,而后取出元素中的数据实体返回而已。