如果对Java容器家族成员不太熟悉,可以先阅读Java容器框架(一)--概述篇这篇文章,LinkedList类在List家族中具有重要的位置,基本上可以和ArrayList平起平坐,在功能上甚至比ArrayList还要强大。下面我们先来看看LinkedList继承关系:
public abstract class AbstractSequentialList extends AbstractList
public class LinkedList
extends AbstractSequentialList
implements List, Deque, Cloneable, java.io.Serializable
从类继承关系可以看到它实现了List接口、Deque接口(是一个双向队列),因此具有的特性也就显而易见。本篇文章从下面方法入手,分析其实现过程从而了解LinkedList的实现原理:
下面我们一一分析这些方法的实现原理。
LinkedList为我们提供了两种构造函数,一个为无参构造,一个传入一个容器作为入参,下面来看看具体的代码实现:
public LinkedList() {
}
public LinkedList(Collection extends E> c) {
this();
addAll(c);
}
暂时不去分析addAll函数,直接看构造函数发现其实什么也没有做,只是另一个容器作为入参的时候,会调用addAll函数,不用看源码也能猜测到是将容器元素添加到当前LinkList容器中,addAll我们接下来会做分析。
add函数相信我们在熟悉不过,向List中添加元素。
那addAll(int index, Collection extends E> c)是什么意思呢?
它是向List容器中第index+1(由于是从0开始计算的)位置开始将容器c中的元素插入到List容器中,List中index+1开始后面的元素后移。
下面我们看看具体的代码实现:
public boolean add(E e) {
linkLast(e);
return true;
}
transient int size = 0; // 链表节点(元素)的个数
transient Node first; // 第一个节点(元素)
transient Node last; // 最后一个节点(元素)
// 链表中每一个元素(节点)
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;
}
}
void linkLast(E e) {
final Node l = last; // 用一个临时变量指向最后一个节点
// 创建一个新的节点,新的节点prev指向当前链表的最后一个元素,next为null
final Node newNode = new Node<>(l, e, null);
last = newNode; // 链表的last指向新的节点,也就是最后一个节点
if (l == null) // 表明刚开始的链表是一个空链表,则first 也指向新创建的节点(因为当前链表只有一个元素)
first = newNode;
else
l.next = newNode; // 实现双向链表
size++;
modCount++;
}
代码中注释非常清楚,add函数中调用的是linkLast函数,也就是向链表末尾添加元素。因此我们在向LinkedList中调用add来添加元素时,默认是添加到链表尾部。
通过函数名大致能够猜测到该函数的作用是向LinkedList中某个位置添加元素,那么具体是怎么实现的,下面看看源码实现:
public void add(int index, E element) {
checkPositionIndex(index); // 检查index 是否合法
// 如果是最后一个位置,则直接最链表尾部添加
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
// 检查index 是否合法
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
// 相当于查找某个位置的节点(元素)
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;
}
}
// 关键是这个函数, 在succ之前插入数据
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++;
}
总结:当向LinkedList的index位置添加元素时,首先判断index位置是否合法,合法则顺序查找index位置的节点(元素),此处的查找有一个小小的优化,只需要顺序查找到链表一半位置即可,找到该节点后,则利用链表的特性直接插入元素,因此在性能上要优于ArrayList,首先LinkedList不需要考虑扩容,其次不需要移动插入位置之后的元素。
public boolean addAll(int index, Collection extends E> c) {
checkPositionIndex(index);
// 为了通用性,可以添加其他类型的数据结构,因此先把传入的c转化为数组;
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node pred, succ;
// 如果是在默认添加
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 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;
}
看代码不难理解,由于传入参数是Collection 类型,因此为了通用,首先转化为具体的数组,然后将数组转化为Node结构添加到链表中,至此将添加元素的相关方法分析完成了。
很明显,这三个函数都是删除元素的作用,那它们具体是怎样实现的呢?其实有了了解添加元素原理的基础,删除元素也就不难了,下面看看具体源码:
public E remove() {
// 默认删除的是链表开始的元素
return removeFirst();
}
public E removeFirst() {
final Node f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node f) {
// assert f == first && f != null;
final E element = f.item;
final Node 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;
}
remove()函数,默认从链表的头部开始删除数据,remove(int index)函数也很容易理解,删除指定位置的元素,此处就不在分析了,比较好奇的是remove(Object o)这个函数,当链表中存在相同的两个元素,那么是如何删除的呢?
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;
}
// 作用是删除x节点,返回对应的值
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; // x节点的数据域、next、prev都设置为null,方便垃圾回收
size--;
modCount++;
return element;
}
从代码可以看到,删除某一个元素是从头部开始查找,当找到时就删除对应节点,即便之后还有相同的元素也不会删除,删除成功则返回true,否则为false。
set函数是用来更新index节点的值,返回旧值,由于存在需要顺序遍历到第index位置,因此时间复杂度为n/2也即为n,源码如下:
public E set(int index, E element) {
checkElementIndex(index); // 检查index 位置的合法性
Node x = node(index); // 遍历获取index位置的节点
E oldVal = x.item;
x.item = element;
return oldVal;
}
get函数是返回index位置节点的数据,同set很类似,也需要遍历到index位置,因此时间复杂度为n/2也即为n,源码实现如下:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
这是返回一个LinkedList的迭代器,通常我们不会直接调用此函数,一般是直接调用List的iterator(),它最终就是调用listIterator(int index),只不过index为0而已,通过迭代器对链表进行遍历,相当于C语言里面的指针一样,指向某个元素顺序遍历,因此复杂度为n。此处就不在展示对应的源码。
我们都知道对List容器进行遍历通常有两种方式,一种为for循环直接遍历,一种通过迭代器方式进行遍历,那么到底哪种遍历方式比较好呢?
- for循环方式遍历
int size = list.size() for(int i=0; i
- 迭代器方式遍历
Iterator iter = list.iterator(); while(iter.hasNext()) { String value = (String)iter.next(); System.out.print(value + " "); }
这两种方式到底哪种性能更优化,还需要看具体是对哪种List容器进行遍历,如果是ArrayList,由于get函数时间复杂度为1,因此采用for循环遍历要优于迭代器方式,如果是LinkedList,由于get函数(上面已经分析过)还需要对List进行遍历找到对应位置,因此采用迭代器方式遍历性能更好,总之,对于数组结构的线性表采用for循环方式遍历,对于链表结构的线性表采用迭代器方式进行遍历。
分析到此处,我们还需要注意一个点,大家知道for和for-each的区别吗?
for循环在熟悉不过,没什么好说的,但是for-each的实现原理有必要了解下,这里只是给出原理,需要知道具体实现请自行探索,for-each循环其实最终是转化为迭代器的遍历方式,我们可以通过对ArrayList遍历查看:
List
list = new ArrayList(); for (Person per:list) { System.out.println(per); } 我们看看最后转化为class文件的代码如下: List list = new ArrayList(); Iterator var5 = list.iterator(); while(var5.hasNext()) { Person per = (Person)var5.next(); System.out.println(per); } 总结:因此我们在遍历ArrayList的时候,最好不要使用for-each而是for,对于LinkedList的遍历,则建议使用for-each或者直接迭代器遍历。
这两个函数其实是属于Deque范畴,在最开始将LinkedList类结构的时候,可以看到LinkedList实现了Deque接口,也即具有双向链表结构。下面看看这两个函数的具体实现,其他也有许多函数,仅此抛砖引玉,代码都很简单。
public void push(E e) {
// 向链表头部添加元素
addFirst(e);
}
public void addFirst(E e) {
linkFirst(e);
}
// 向头部增加节点
private void linkFirst(E e) {
final Node f = first;
final Node newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
// 以上为push的实现
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
总结:以上其实无非都是对链表进行操作,只是push和pop都是对头部节点进行操作,因此类似于栈的功能。
至此LinkedList的源码分析就结束了,LinkedList是基于双向链表实现,可以快速插入删除元素,由于保存有链表头部和尾部的应用(C/C++ 角度可以理解为指针),因此可以方便实现队列和栈的功能,同时在遍历链表时,建议使用迭代器来完成,而不是通过for+get(index)这种形式来遍历。