Java容器框架(三)--LinkedList实现原理

1. 简介

如果对Java容器家族成员不太熟悉,可以先阅读Java容器框架(一)--概述篇这篇文章,LinkedList类在List家族中具有重要的位置,基本上可以和ArrayList平起平坐,在功能上甚至比ArrayList还要强大。下面我们先来看看LinkedList继承关系:

public abstract class AbstractSequentialList<E> extends AbstractList<E> 

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

从类继承关系可以看到它实现了List接口、Deque接口(是一个双向队列),因此具有的特性也就显而易见。本篇文章从下面方法入手,分析其实现过程从而了解LinkedList的实现原理:

下面我们一一分析这些方法的实现原理。

2. LinkedList()&LinkedList(Collection<? extends E> c)

LinkedList为我们提供了两种构造函数,一个为无参构造,一个传入一个容器作为入参,下面来看看具体的代码实现:

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
}

暂时不去分析addAll函数,直接看构造函数发现其实什么也没有做,只是另一个容器作为入参的时候,会调用addAll函数,不用看源码也能猜测到是将容器元素添加到当前LinkList容器中,addAll我们接下来会做分析。

3. add(E e)&add(int index, E element)&addAll(int index, Collection<? extends E> c)

add函数相信我们在熟悉不过,向List中添加元素。

那addAll(int index, Collection<? extends E> c)是什么意思呢?

它是向List容器中第index+1(由于是从0开始计算的)位置开始将容器c中的元素插入到List容器中,List中index+1开始后面的元素后移。

下面我们看看具体的代码实现:

  • add(E e)
public boolean add(E e) {
     linkLast(e);
     return true;
}

transient int size = 0;   // 链表节点(元素)的个数
transient Node<E> first;  // 第一个节点(元素)
transient Node<E> last;    // 最后一个节点(元素)

// 链表中每一个元素(节点)
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;
    }
}

void linkLast(E e) {
    final Node<E> l = last;    // 用一个临时变量指向最后一个节点
    // 创建一个新的节点,新的节点prev指向当前链表的最后一个元素,next为null
    final Node<E> 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来添加元素时,默认是添加到链表尾部。

  • add(int index, E element)

通过函数名大致能够猜测到该函数的作用是向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<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;
    }
}

// 关键是这个函数, 在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的index位置添加元素时,首先判断index位置是否合法,合法则顺序查找index位置的节点(元素),此处的查找有一个小小的优化,只需要顺序查找到链表一半位置即可,找到该节点后,则利用链表的特性直接插入元素,因此在性能上要优于ArrayList,首先LinkedList不需要考虑扩容,其次不需要移动插入位置之后的元素。

  • addAll(int index, Collection<? extends E> c)
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<E> 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<E> 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结构添加到链表中,至此将添加元素的相关方法分析完成了。

4. remove()&remove(int index)&remove(Object o)

很明显,这三个函数都是删除元素的作用,那它们具体是怎样实现的呢?其实有了了解添加元素原理的基础,删除元素也就不难了,下面看看具体源码:

  • remove()
public E remove() {
    // 默认删除的是链表开始的元素
    return removeFirst();
}

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;
    if (next == null)    // 这种情况是由于链表中只有一个元素,被删除之后
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

remove()函数,默认从链表的头部开始删除数据,remove(int index)函数也很容易理解,删除指定位置的元素,此处就不在分析了,比较好奇的是remove(Object o)这个函数,当链表中存在相同的两个元素,那么是如何删除的呢?

  • remove(Object o)
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;
}

// 作用是删除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;    // x节点的数据域、next、prev都设置为null,方便垃圾回收
        size--;
        modCount++;
        return element;
}

从代码可以看到,删除某一个元素是从头部开始查找,当找到时就删除对应节点,即便之后还有相同的元素也不会删除,删除成功则返回true,否则为false。

5. set(int index, E element)&get(int index)&listIterator(int index)

  • set(int index, E element)

set函数是用来更新index节点的值,返回旧值,由于存在需要顺序遍历到第index位置,因此时间复杂度为n/2也即为n,源码如下:

public E set(int index, E element) {
     checkElementIndex(index);    // 检查index 位置的合法性
     Node<E> x = node(index);    // 遍历获取index位置的节点
     E oldVal = x.item;
     x.item = element;
     return oldVal;
}
  • get(int index)

get函数是返回index位置节点的数据,同set很类似,也需要遍历到index位置,因此时间复杂度为n/2也即为n,源码实现如下:

 public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
  • listIterator(int index)

这是返回一个LinkedList的迭代器,通常我们不会直接调用此函数,一般是直接调用List的iterator(),它最终就是调用listIterator(int index),只不过index为0而已,通过迭代器对链表进行遍历,相当于C语言里面的指针一样,指向某个元素顺序遍历,因此复杂度为n。此处就不在展示对应的源码。

我们都知道对List容器进行遍历通常有两种方式,一种为for循环直接遍历,一种通过迭代器方式进行遍历,那么到底哪种遍历方式比较好呢?

  • for循环方式遍历
int size = list.size()
for(int i=0; i<size;i++){                
    System.out.println(list.get(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<Person> list = new ArrayList();
for (Person per:list) {
    System.out.println(per);
}
我们看看最后转化为class文件的代码如下:

List<Person> 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或者直接迭代器遍历。

6. push(E e)&pop()

这两个函数其实是属于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<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++;
}

// 以上为push的实现

public E pop() {
    return removeFirst();
}

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

总结:以上其实无非都是对链表进行操作,只是push和pop都是对头部节点进行操作,因此类似于栈的功能。

总结

至此LinkedList的源码分析就结束了,LinkedList是基于双向链表实现,可以快速插入删除元素,由于保存有链表头部和尾部的应用(C/C++ 角度可以理解为指针),因此可以方便实现队列和栈的功能,同时在遍历链表时,建议使用迭代器来完成,而不是通过for+get(index)这种形式来遍历。

猜你喜欢

转载自blog.csdn.net/u010349644/article/details/82868722
今日推荐