您的位置:首页 > 编程语言 > Java开发

JAVA源码学习-LinkedList

2016-01-20 00:00 579 查看
摘要: 很多公司的面试题都会问到ArrayList和LinkedList的区别,在这里我先稍微做下总结。他们都是List的实现类,实现方式上ArrayList是用数组,LinkedList是用双向链表,性能上ArrayList随机访问效率高,但插入和删除效率低,而LinkedList随机访问效率低,但插入和删除效率高,这样的性能又决定了他们的使用场景不同。那么是什么导致这哥俩性能那么互补呢?希望这篇对LinkedList的源码解读能提供这个问题的答案。

很多公司的面试题都会问到ArrayList和LinkedList的区别,在这里我先稍微做下总结。他们都是List的实现类,实现方式上ArrayList是用数组,LinkedList是用双向链表,性能上ArrayList随机访问效率高,但插入和删除效率低,而LinkedList随机访问效率低,但插入和删除效率高,这样的性能又决定了他们的使用场景不同。那么是什么导致这哥俩性能那么互补呢?希望这篇对LinkedList的源码解读能提供这个问题的答案。

按惯例先上LinkedList类的定义:

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

跟ArrayList继承自AbstractList不同,LinkedList继承自AbstractSequentiaList,与ArrayList一样都实现了List,Cloneable和Serializable接口,都可以克隆并序列化,不一样的是实现了Deque接口,这个接口定义了跟双端队列相关的操作。

LinkedList通过链表实现,那么肯定以一个链表节点作为基础数据单元,跟ArrayList的数组不一样,这个链表节点的定义如下:

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之中定义了三个属性:

transient int size = 0;

/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
*            (first.prev == null && first.item != null)
*/
transient&n
7fe0
bsp;Node<E> first;

/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
*            (last.next == null && last.item != null)
*/
transient Node<E> last;

size属性记录了LinkedList中存储的元素个数,first和last分别是LinkedList的头节点和尾节点。

LinkedList的构造方法有两个

/**
* Constructs an empty list.
*/
public LinkedList() {
}

/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param  c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}


ArrayList中的无参构造方法(默认构造函数)还默认初始化一个长度为10的数组,LinkedList中的无参构造方法啥也没有,这该如何理解?从这里看,用无参构造函数初始化的时候貌似什么也没做,其实不然,仔细看上面提到的Node类,它通过静态加载方式声明,即在工程加载时变初始化了Node类,并包含了节点,前驱,后继三个属性;在初始化LinkedList时,又将这三个属性赋给first和last,并且都为null,即得到了一个空链表。构建对象较完整的顺序是:

先在工程加载时初始化静态代码(Node类),new LinkedList()时,先执行父类的非静态代码初始化父类成员变量,然后执行父类构造方法,接着执行子类成员变量,最后才执行子类构造方法,继而完成了对象的创建。

带参数的构造方法里有一个addAll(Collection), 即将Collection转换为LinkedList所能接受的数据(节点),将这些节点追加到linkedList中。

addAll(int, Collection),重载addAll(Collection),在指定位置插入Collection到linkedList中

public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);//如果index小于0且大于size,则抛出IndexOutOfBoundsException异常。

Object[] a = c.toArray();//将Collection转换为对象数组
int numNew = a.length;
if (numNew == 0)//如果待插入对象为空,则直接返回。这里为什么不直接使用a!=null?原因我的理解是执行效率问题和不容易出错
return false;

Node<E> pred, succ;//根据index查找对应节点信息,以确定Collection的始末位置。pred为起始位置,succ为结尾位置,可将其看作两个指针。
if (index == size) {//如果index==size,即在链表末尾添加元素,则Collection的起始位置为链表的last,结尾位置为null
succ = null;
pred = last;
} else {//否则,结尾位置为index对应的那个节点succ,起始位置为succ的前驱节点。
succ = node(index);
pred = succ.prev;
}

for (Object o : a) {//对Collection中每一个元素创建节点newNode,使其前驱为pred,后继null
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)//如果pred==null,则说明为第一个元素,first指向newNode
first = newNode;
else             //否则,起始位置pred的后继节点为newNode
pred.next = newNode;
pred = newNode;,//变更新的起始位置为newNode
}//该for循环执行之后会将确定好的起始位置的节点,Collection中每个元素创建的节点连接起来,此时pred存放着所有Collection添加完成后的末尾位置

if (succ == null) {//如果结尾位置(即index对应的那个节点)为null,说明是在链表结尾添加,则将last指针指向succ的前置
last = pred;
} else {           //否则将所有Collection添加完成后的末尾位置pred的后继指向index对应的节点,index节点的前驱指向pred
pred.next = succ;
succ.prev = pred;
}

size += numNew;//链表长度增加Collection的长度
modCount++;
return true;
}

构造方法调用的addAll最终会将size作为index参数调用addAll(int index, Collection<? extends E> c),怎么实现的等看懂了以后再写。

接下来就是几个比较重要的私有方法,LinkedList的大多数公有方法里都会出现他们的身影。

linkFirst(E e),将e插入为第一个元素

/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}


这里我将first,last理解为C语言里的指针,将first指向的元素复制给Node f,声明一个newNode节点,使其前驱为null,节点元素为e,后继为f(此时的f为原来链表里第一个元素),此时已经完成了头结点->newNode->f的链接,在将first的指针位置更新到newNode,是newNode成为链表第一个元素。如果链表第一个元素为null,即空链表,则将last也指向newNode;否则,使f的前驱指向newNode,建立从f-》newNode的链接。同时将LinkedList的size加一。

linkLast(E e),将e插入为最后一个元素

/**
* Links e as last element.
*/
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++;
}


过程跟linkFirst类似。先将LinkedList最后一个元素拷贝至l,初始化一个newNode使其前驱为l,元素为e,后继为null,此时完成从newNode->l的链接;更新last指针为newNode;如果原LinkedList最后一个节点为null(即为空链表),将first也指向newNode;否则,将原最后一个节点的后继指向newNode,完成l->newNode的链接,同时将LinkedList的size加一。

linkBefore(E e, Node<E> succ),插入一个元素到succ之前,如果succ的前置为null(即succ为第一个节点),则同linkFirst方法。

/**
* Inserts element e before non-null 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++;
}


流程是:先将succ的前驱元素复制给pred,初始化newNode节点使其前驱为pred,元素为e,后继为succ,此时建立了从newNode->succ的前置元素pred,newNode到succ的链接;建立succ->newNode的链接;如果succ的前置元素为null,则令newNode为一个元素;否则令succ前置元素的后继指向newNode,建立pred->newNode的链接,同时将LinkedList的size加一。

为什么没有类似linkAfter的方法,个人理解这跟linkBefore功能上重合,ORACLE的程序猿们懒得写~


以上linkFirst,linkLast,linkBefore可视作LinkedList的插入方法,过程总结如下:

先找出待插入位置的元素,另存副本A;新建node节点B与A链接,并调整收尾指针first和last,最后size加1。按照这个思路在比较这三个方法,发现linkFirst对应了linkBefore的一种特殊情况,即linkBefore的succ元素的前驱为null,即succ为链表第一个元素。

接下来我们看一组unlink方法,unlink方法顾名思义就是将LinkedList中的某个元素移除,一般思路是先将这个元素的左右链接断开,然后将这个元素的前驱后继元素链接上。

unlinkFirst(Node<E> f)

/**
* Unlinks non-null first node 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;
}


既然是移除linkedList的第一个元素,那么这个方法在供其他方法使用的时候,f肯定会是first指针指向的元素。

备份第一个元素f的item和后继元素,然后将f的item和后继指针指向空,即从该链表中断开链接。将first指针指向原有链表第二个元素,即f的后继。如果f的后继也为null,那说明移除后成了空链表,尾指针last也指向null,否则将f的后继元素的前驱指向null,同时size减一,返回删除节点的元素值。

unlinkLast(Node<E> l)

/**
* Unlinks non-null last node 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;
size--;
modCount++;
return element;
}


类似的,unlinkLast的入参l肯定会是last指向的尾节点

备份尾节点的元素值及其前驱节点,然后将l的item和前驱指针指向空,让此节点从LinkedList中断开,将last指向l的前驱节点。如果前驱节点为空,说明移除后成了空链表,则让first也指向null,否则l前置节点的后继指向空,同时将size减一。

unlink(Node<E> x):删除LinkedList中任意非空节点x

/**
* Unlinks non-null node x.
*/
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;
}

先备份x的元素值,前驱节点和后继节点;如果x的前驱为null(即x为链表第一个节点),则让first指向x的后继节点,否则x前驱节点的后继指向x的后继节点,并将x的前驱断开;如果x的后继节点为null(即x为链表最后一个元素),则让last指向x的前驱节点,否则x的后继节点的前驱指向x的前驱节点,并将x的后继断开;同时将x的元素值置空,size减一并返回移除的节点元素值。

从上面的插入删除方法可以看到,链表的插入删除操作是比ArrayList效率要高的。那为什么链表的查找效率比ArrayList低,那是因为ArrayList支持随机访问而链表不能,那链表是如何查找的呢?看下面的方法:

node(int index)

/**
* Returns the (non-null) Node at the specified element index.
*/
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;
}
}


根据提供的元素序号index,使用二分法在链表中循环遍历对应的元素,因为链表是无序集合,只能按照每个节点的后继去挨个查找。就是说每次对LinkedList做查找,排除最好的情况,LinkedList都得或多或少把自个元素挨个点个到,这种查找方式由链表的特性决定,实属无奈之举,所以效率没ArrayList高。

理解了上面几个方法,那么下面的方法就容易多了,无非是对以上方法的封装,增加一些校验而已,这里就简写了。

getFirst(),getLast():返回链表第一个元素值和最后一个元素值,不是节点。

removeFirst(),removeLast():分别调用unlinkFirst和unlinkLast移除第一个和最后一个节点,返回节点值。

addFirst(E),addLast(E):分别调用linkFirst和linkLast增加第一个节点和最后一个节点,返回节点值。

contains(Object o):分别调用indexOf(o)对LinkedList进行遍历查找对应的元素o,没有用到node(int index)方法。

add(E):调用linkFirst第一个节点,返回true。

remove(Object):遍历LinkedList,找到Object对应的节点,使用unlink方法移除,移除成功返回true,没有该元素则返回false。

等等,分别实现由List的增删查方法,队列的增删查方法和双端队列的增删查方法,太多了,这里不一一列举了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: