Java集合框架-LinkedList的源码解析

667 阅读14分钟

一、前言

站在巨人的肩膀上,本系列的Java集合框架文章参考了skywang12345——Java 集合系列,真心讲的很好,可以参考。但是不足的是,时间过于长久了,大佬的文章是基于JDK1.6.0_45,对于现在来说至少都用JDK 8.0以上了,而且JDK 6.0与JDK 8.0中集合框架的改动挺大的,所以本系列的文章是基于JDK_1.8.0_161进行说明的。

二、介绍

我们先来看看LinkedList的类定义,以及继承关系:

java.util.Collection<E>
    -> java.util.AbstractCollection<E>
        -> java.util.AbstractList<E>
            -> java.util.AbstractSequentialList<E>
            	-> java.util.LinkedList<E>

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • LinkedList是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
  • LinkedList实现 List 接口,能对它进行队列操作。
  • LinkedList实现 Deque 接口,即能将LinkedList当作双端队列使用。
  • LinkedList实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  • LinkedList实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
  • LinkedList它是非线程安全的,是非同步的。

再来从整体看一下LinkedList与Collection关系,如下图:

LinkedList的本质是双向链表。
(01) LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。
(02) LinkedList包含三个重要的成员:first、last和size:
first是指向头节点的指针、last是指向末尾节点的指针。它们都是是双向链表节点所对应的类Node的实例。Node中包含成员变量: prev,next,item。其中,prev是该节点的上一个节点,next是该节点的下一个节点,item是该节点所包含的值。
size是双向链表中节点的个数。

三、解析

对源码的解析,我个人喜欢从方法的使用上去看,而不是把源码所有的方法从头到尾的都去看一遍,除非是那种比较短的源码还行,如果是上千行代码的话,我觉得会很崩溃的。这样不仅学习效率底下,而且看源码本身就是一件非常枯燥的事情,容易看着看着就不想看下去了(大佬忽略),非常打击学习源码的兴趣。

1、首先我们来看看它的成员变量

//链表的长度
transient int size = 0;
//指向头节点的指针
transient Node<E> first;
//指向末尾节点的指针
transient Node<E> last;

2、接着来看看它的构造方法,LinkedList它有2个构造方法。

//构造一个空链表
public LinkedList() {
}

//传递一个集合
public LinkedList(Collection<? extends E> c) {
	//也会调用空的构造方法
	this();
	addAll(c);
}

// 将“集合(c)”添加到LinkedList中。
// 实际上,是从双向链表的末尾开始,将“集合(c)”添加到双向链表中。
public boolean addAll(Collection<? extends E> c) {
	return addAll(size, c);
}

// 从双向链表的index开始,将“集合(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 位置的前置和后置节点
	if (index == size) {
		succ = null;
		pred = last;
	} else {
		succ = node(index);
		pred = succ.prev;
	}

	for (Object o : a) {
		@SuppressWarnings("unchecked") 
        E e = (E) o;
		Node<E> newNode = new Node<>(pred, e, null);
		if (pred == null) //如果前置节点为null
			first = newNode;
		else
        	//前置节点链接新节点
			pred.next = newNode;
        //更新循环时的 prev 为最新链接的节点
		pred = newNode;
	}

	if (succ == null) {
		last = pred;
	} else {
		pred.next = succ;
		succ.prev = pred;
	}

	//更新链表大小
	size += numNew;
	modCount++;
	return true;
}

//LinkedList节点类
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;
	}
}

3、当构造一个空的LinkedList后,我们肯定需要添加数据元素,那就去看看添加元素方法。添加方法有3组:add(E e)和add(int index, E element)、addAll(int index, Collection<? extends E> c)、addFirst(E e)和addLast(E e)。先来看第一组:

//添加元素
public boolean add(E e) {
	linkLast(e);
	return true;
}

//链接到LinkedList末尾
void linkLast(E e) {
	//先获取末尾节点
	final Node<E> l = last;
	//把传递进来的e元素创建一个新节点,并把新节点的前节点设置成last节点
	final Node<E> newNode = new Node<>(l, e, null);
	//更新末尾节点
	last = newNode;
	if (l == null)
		//这里如果是首次添加进来就会进入这个语句,则把新创建的节点设置成首节点
		first = newNode;
	else
		//后面添加进来的元素节点就把末尾节点的下一个节点设置成新创建的节点
		l.next = newNode;
	//更新长度
	size++;
	//更新修改次数
	modCount++;
}

//把数据插入到特定的索引中去
public void add(int index, E element) {
	//检查插入的下标是否合法
	checkPositionIndex(index);

	if (index == size) //如果下标等于列表的长度,则直接插入数据到末尾
		linkLast(element);
	else //否则插入数据到索引值所对应的节点之前
		linkBefore(element, node(index));
}

//插入数据到某个节点之前
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;
	//当前节点是null时,就把这个新节点赋值给头节点
	if (pred == null)
		first = newNode;
	else
		//把pred节点的下一个节点指向这个新节点
		pred.next = newNode;
	//更新列表长度
	size++;
	//更新修改次数
	modCount++;
}

//根据下标索引获取对应的节点
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;
	}
}

这里用add(E e)方法做具体的说明。
(1)add方法添加数据时,直接调用了linkLast方法后返回了true;
(2)从方法名我们可以得知,就是把添加进来的元素链接到链表的末尾,看看linkLast。首先获取了链表的末尾节点,然后根据传入的元素值创建一个新节点,并把新节点的prev设置成last节点,把新节点的next设置成null。
(3)把新节点重新赋值给末尾节点,如果是首次添加数据,那么末尾节点肯定是null的,那么就会进入if语句,就把新节点设置成链表的头节点;如果不是首次添加数据,就会进入else语句,把原链表末尾节点的下一个节点指向新节点,此时这个新节点就是整个链表的末尾节点;
(4)更新链表长度,更新修改次数;

add(int index, E element)把数据添加到指定索引下的方法逻辑是类似的,就不再具体说明了,注释已经说的很明白了。

第二组在构造方法那里已经说过了,就不再赘述。接着看第三组。

//添加元素到头部
public void addFirst(E e) {
	linkFirst(e);
}

//链接元素至头部
private void linkFirst(E e) {
	//先获取头节点
	final Node<E> f = first;
	//创建新节点,并将新节点的下一个节点设置成头节点,前节点设置成null
	final Node<E> newNode = new Node<>(null, e, f);
	//更新头节点
	first = newNode;
	if (f == null) //如果头节点为null,则把新节点设置到末尾节点
		last = newNode;
	else //否则,把头节点的前一个节点设置成新节点
		f.prev = newNode;
	//更新列表长度
	size++;
	//更新修改次数
	modCount++;
}

//添加元素到末尾,这个方法和add(E e)一样,只不过此方法没有返回值
public void addLast(E e) {
	linkLast(e);
}

这里用addFirst(E e)方法做具体说明。
(1)调用addFirst方法,直接调用了linkFirst方法;
(2)根据方法名我们可以猜测,是将添加进来的元素放置到链表的头部。首先获取了头节点,并根据传入的元素值,创建一个新节点,把新节点的前节点置为null,把新节点的下个节点设置成头节点;
(3)把新节点更新为头节点;
(4)如果头节点为null,则把新节点设置到末尾节点;否则,把头节点的前节点设置成新节点,那么此时新节点就成为了整个链表的头节点;
(5)更新链表长度,更新修改次数。

4、继续看看删除方法,删除元素的方法有很多,也是分为了3组:remove(int index)和remove(Object o)、removeFirst()和removeLast()、removeFirstOccurrence(Object o)和removeLastOccurrence(Object o),那么首先来看第一组:

//删除指定元素
public boolean remove(Object o) {
	//这里首先判断元素是否为null,这也就说明了我们可以添加null到LinkedList
	if (o == null) {
		for (Node<E> x = first; x != null; x = x.next) {
			//从头节点开始遍历
			if (x.item == null) {
				//解除x节点的链表链接
				unlink(x);
				//找到了就返回true
				return true;
			}
		}
	} else {
		//若不为null。也是从头节点开始遍历
		for (Node<E> x = first; x != null; x = x.next) {
			//如果x节点的值等于目标元素值
			if (o.equals(x.item)) {
				unlink(x);
				//找到了就返回true
				return true;
			}
		}
	}
	//没有找到,则返回false
	return false;
}

//解除x节点的链表链接
E unlink(Node<E> x) {
	// assert x != null;

	//获取x节点的值
	final E element = x.item;
	//获取x节点的下个节点
	final Node<E> next = x.next;
	//获取x节点的前节点
	final Node<E> prev = x.prev;

	//若前节点为null
	if (prev == null) {
		//重置x节点的下个节点为头节点
		first = next;
	} else {
		//否则,把x节点的下个节点设置成x节点的前个节点的下个节点
		prev.next = next;
		//把x节点的前节点置空
		x.prev = null;
	}

	//若下个节点为null
	if (next == null) {
		//把前节点设置给末尾节点
		last = prev;
	} else {
		//否则,把前节点设置到下个节点的前节点
		next.prev = prev;
		//把x节点的下个节点置空
		x.next = null;
	}

	//把x节点的元素值置空,那么x节点的前节点,后节点,元素值都是null了,这也就脱离了LinkedList链表
	x.item = null;
	//更新长度-1
	size--;
	//更新修改次数
	modCount++;
	//返回x节点的元素值
	return element;
}

//根据下标索引删除节点
public E remove(int index) {
	//检查下标是否合法
	checkElementIndex(index);
	//解除x节点的链表链接
	return unlink(node(index));
}

//jdk 1.5加入的方法。尝试删除头节点,并返回节点数据
public E remove() {
	return removeFirst();
}

这里用remove(Object o)方法来做具体说明。
(1)先判断参数是否为null,这也就说明了在LinkedList中,可以添加null到链表中。
(2)若为null,从头节点开始遍历,查询节点中的值是否为null,若查询到了,会调用unlink方法后返回true;
(3)unlink方法名,说明是解除链表的链接;先获取了节点x的元素值、前节点、下个节点;
(4)若前节点为空,说明要删除的x节点就是当前链表的头节点,就把x节点的下个节点设置成头节点;否则x节点的下个节点,设置成x节点前节点的下个节点,x节点的前节点置为null(这里有点绕,需要自己画个图思考下);
(5)若下个节点为空,说明要删除的x节点就是当前链表的末尾节点,就把x节点的前节点设置成末尾节点;否则x节点的前节点,设置成x节点下个节点的前节点,然后把x节点的下个节点置为null(这里有点绕,需要自己画个图思考下);
(6)把x节点的元素置为null,这样x节点的前节点、下个节点、元素都是null,就可以让GC回收了;把链表长度-1;更新修改次数;最后把x节点的元素值返回。

接着来看看删除元素的第二组方法:removeFirst()和removeLast()

//删除头节点,并返回节点元素值
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;
	//获取头节点的下个节点
	final Node<E> next = f.next;
	//把头节点的元素置空
	f.item = null;
	//把头节点的下个节点置空
	f.next = null; // help GC
	//把下个节点设置成头节点
	first = next;
	//如果头节点的下个节点为null,则把末尾节点也置空
	if (next == null)
		last = null;
	else
		//否则,下个节点的前节点置空
		next.prev = null;
	//更新长度-1
	size--;
	//更新修改次数
	modCount++;
	return element;
}

//删除末尾节点
public E removeLast() {
	//获取末尾节点
	final Node<E> l = last;
	if (l == null)
		throw new NoSuchElementException();
	//解除末尾节点的链接
	return unlinkLast(l);
}

//解除末尾节点的链接
private E unlinkLast(Node<E> l) {
	// assert l == last && l != null;

	//获取末尾节点的元素数据
	final E element = l.item;
	//获取末尾节点的前节点
	final Node<E> prev = l.prev;
	//把末尾节点的元素数据置空
	l.item = null;
	//把末尾节点的前节点置空
	l.prev = null; // help GC
	//把前节点设置成列表的末尾节点
	last = prev;
	//如果前节点为空
	if (prev == null)
		//把头节点置空
		first = null;
	else
		//否则把前节点的下个节点置空
		prev.next = null;
	//更新长度-1
	size--;
	//更新修改次数
	modCount++;
	return element;
}

这里就用removeFirst()方法来做说明:
(1)根据方法名我们可以猜测,是删除链表的头部节点;首先获取头节点,判断是否为空;接着调用unlinkFirst方法,把头节点传入。
(2)首先获取了头节点的元素值、头节点的下个节点;把头节点的元素值和头节点的下个节点置为null;
(3)把头节点的下个节点赋值给头节点,这样就更新了头节点信息;
(4)如果头节点的下个节点为null,说明整个链表中只有1个元素节点,那么就把末尾节点置为null;
(5)否则,就把头节点的下个节点的前节点置为null;
(6)把链表长度-1;更新修改次数;返回头节点的元素值;

最后来看看第三组删除元素的方法:removeFirstOccurrence(Object o)和removeLastOccurrence(Object o),这两个方法都是jdk 1.6新增加的。

/**
 * @since 1.6 删除此列表中第一个出现的指定元素
 */
public boolean removeFirstOccurrence(Object o) {
	return remove(o);
}

/**
 * @since 1.6 删除此列表中指定元素的最后一个匹配项
 */
public boolean removeLastOccurrence(Object o) {
	//这里的逻辑和remove(Object o)方法中的逻辑时相似的,只不过这里是从末尾节点开始遍历的。
	if (o == null) {
		for (Node<E> x = last; x != null; x = x.prev) {
			if (x.item == null) {
				unlink(x);
				return true;
			}
		}
	} else {
		for (Node<E> x = last; x != null; x = x.prev) {
			if (o.equals(x.item)) {
				unlink(x);
				return true;
			}
		}
	}
	return false;
}

removeFirstOccurrence(Object o)和removeLastOccurrence(Object o)里面的逻辑和之前的逻辑类似,就不再具体说明了。

5、继续了解LinkedList获取元素的get操作和修改元素的set操作。其中get方法有3个。

//通过索引获取元素值
public E get(int index) {
	//检查下标是否合法
	checkElementIndex(index);
	//返回索引对于的节点,并获取节点的元素值并返回
	return node(index).item;
}

//获取头节点元素值
public E getFirst() {
	//获取头节点
	final Node<E> f = first;
	if (f == null)
		throw new NoSuchElementException();
	//直接返回头节点的元素值
	return f.item;
}

//获取末尾节点元素值
public E getLast() {
	//获取末尾节点
	final Node<E> l = last;
	if (l == null)
		throw new NoSuchElementException();
	//直接返回末尾节点的元素值
	return l.item;
}

//通过下标索引更新索引对应节点的元素值
public E set(int index, E element) {
	//检查下标是否合法
	checkElementIndex(index);
	//根据索引获取节点x
	Node<E> x = node(index);
	//获取x节点的元素值
	E oldVal = x.item;
	//更新x节点的元素值
	x.item = element;
	//返回旧的元素值
	return oldVal;
}

6、LinkedList其他常用的方法。

//清空链表数据
public void clear() {
	//从头开始遍历
	for (Node<E> x = first; x != null; ) {
		Node<E> next = x.next;
		把x节点的元素值、前节点、下个节点都设置为null
		x.item = null;
		x.next = null;
		x.prev = null;
		x = next;
	}
	//把链表的头节点、末尾节点设置为null
	first = last = null;
	//长度设置为0
	size = 0;
	modCount++;
}

//是否包含某个元素值
public boolean contains(Object o) {
	return indexOf(o) != -1;
}

//根据元素值返回节点的下标索引
public int indexOf(Object o) {
	int index = 0;
	//先判断元素值是否为null
	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;
}

//把列表转化成Object类型的数组
public Object[] toArray() {
	//先创建一个和列表长度相等的object数组
	Object[] result = new Object[size];
	int i = 0;
	//从头节点开始遍历,把节点的元素值赋值给数组中对应的位置
	for (Node<E> x = first; x != null; x = x.next)
		result[i++] = x.item;
	return result;
}

//把列表转化成对应类型的数组
public <T> T[] toArray(T[] a) {
	//如果传入的对应类型的数组长度小于了列表的长度,通过反射转化列表对应类型的数组,并初始化长度
	if (a.length < size)
		a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);
	int i = 0;
	Object[] result = a;
	//从头开始遍历,并添加元素到数组中
	for (Node<E> x = first; x != null; x = x.next)
		result[i++] = x.item;

	//超过的长度部分直接设置成null
	if (a.length > size)
		a[size] = null;
	return a;
}

7、LinkedList当成栈(后进先出-LIFO)来使用时,使用到的相关方法:

//把元素添加到列表头部,相当于入栈,注意此方法是jdk 1.6添加的
public void push(E e) {
	//直接调用了addFirst方法,那么也可以直接调用addFirst
	addFirst(e);
}

//从列表头部删除元素,相当于出栈,注意此方法是jdk 1.6添加的
public E pop() {
	//直接调用了removeFirst方法,那么也可以直接调用removeFirst
	return removeFirst();
}

//如果列表不为null,那么从列表头部删除元素,相当于出栈,注意此方法是jdk 1.5添加的
public E poll() {
	final Node<E> f = first;
	return (f == null) ? null : unlinkFirst(f);
}

/**
 * 如果列表不为空,那么获取栈顶元素值,注意此方法是jdk 1.5添加的
 * @since 1.5
 */
public E peek() {
	final Node<E> f = first;
	return (f == null) ? null : f.item;
}

/**
 * 此方法和peek()时完全一样的,注意此方法是jdk 1.6添加的
 * @since 1.6
 */
public E peekFirst() {
	final Node<E> f = first;
	return (f == null) ? null : f.item;
}

/**
 * 返回栈底元素值,注意此方法是jdk 1.6添加的
 * @since 1.6
 */
public E peekLast() {
	final Node<E> l = last;
	return (l == null) ? null : l.item;
}

8、LinkedList当初队列(先进先出-FIFO)来使用时,使用到的相关方法:

/**
 * 添加元素到链表末尾,即添加到队列末尾
 * @since 1.5
 */
public boolean offer(E e) {
	return add(e);
}

/**
 * 添加元素到链表末尾,即添加到队列末尾
 * @since 1.6
 */
public boolean offerLast(E e) {
	addLast(e);
	return true;
}

/**
 * 添加元素到链表头部,即添加到队头
 * @since 1.6
 */
public boolean offerFirst(E e) {
	addFirst(e);
	return true;
}

//删除队头元素,也可以使用remove()方法
public E poll() {
	final Node<E> f = first;
	return (f == null) ? null : unlinkFirst(f);
}

以上就是我们在使用LinkedList会经常使用到的方法,其他方法就不再说明了,可以自己去看看。

四、总结

  • LinkedList 实际上是通过双向链表去实现的。它包含一个非常重要的内部类:Node。Node是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
  • 从LinkedList的实现方式中可以发现,没有扩容的相关方法,也就不存在LinkedList容量不足的问题。
  • LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
  • LinkedList实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
  • 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。

五、参考

Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例