Java集合类库 LinkedList 源码解析

来源:互联网 发布:power map for mac 编辑:程序博客网 时间:2024/05/19 14:40

基于JDK 1.7,和ArrayList进行比较分析

Java已经有了ArrayList,用来存放元素,对元素的操作都很方便。为什么还会有LinkedList呢?我们都知道ArrayList获取元素很快,但是插入一个元素很慢,因为ArrayList底层维护的是一个数组,往数组中的某个位置插入一个元素,是很消耗资源的。

而LinkedList插入元素很快,获取任意位置的元素却很慢。这是为什么呢?底层又是怎样实现的呢?

1.继承关系

LinkedList的继承关系图:

这里写图片描述

LinkedList继承的是AbstractSequentialList抽象类,而ArrayList继承的是AbstractList抽象类,也就是AbstractSequentialList类的上一层。

那么我们就去看看AbstractSequentialList抽象类到底做了哪些操作:

这里写图片描述

发现这个类其实很简单,一个无参的构造方法和7个方法,其实每个方法的实现都很简单,简单描述一下原理:通过迭代器来对列表进行增删改查。首先得到ListIterator,ListIterator的next()方法得到当前元素,set()方法修改元素,remove()方法删除元素。

// 这是set()方法的实现public E set(int index, E element) {      try {          ListIterator<E> e = listIterator(index);          E oldVal = e.next();          e.set(element);          return oldVal;      } catch (NoSuchElementException exc) {          throw new IndexOutOfBoundsException("Index: "+index);      }  }

2.实现接口

我们意外的发现LinkedList实现了Deque< E >接口,这个接口是用来干嘛的?从字面上的意思分析deque是双向队列的意思,拿到Deque< E >里面是队列的缺省方法,马上去追踪一下。

public interface Deque<E> extends Queue<E> {

马上明白了,Deque< E >继承自Queue< E >,那Queue肯定是队列了,不信就进去看一看。

public interface Queue<E> extends Collection<E> {

这里写图片描述

Queue< E >继承Collection接口,并添加特有方法,介绍一些这几个方法。

add(E e) – 将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。

offer(E e) – 将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E e),后者可能无法插入元素,而只是抛出一个异常。

remove() – 获取并移除此队列的头。

poll() – 获取并移除此队列的头,如果此队列为空,则返回 null。

element() – 获取但是不移除此队列的头。

peek() – 获取但不移除此队列的头,如果此队列为空,则返回 null。

3.LinkedList的介绍

LinkedList是一个链接列表,实现List所有可选的列表操作,并且允许操作所有元素(包括 null)。除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。

由于LinkedList实现 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。

所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。

注意:LinkedList是线程不同步的。

在数据结构中,我们都知道有链表这种数据类型,典型的先进先出操作FIFO,像火车进站一样,先进的先出来。链表也分单向链表和双向链表,又分循环链表和非循环链表。只要明白了链表的实现原理,LinkedList是怎样实现的就一目了然了。

首先看一下最简单的单向链表的实现

这里写图片描述

元素n1指向n2,n2指向n3,如果有无穷多个元素,就这样一直循环下去。

package com.zhou.collection_11;public class SingleLinkedListDemo {    public static void main(String[] args) {        Node n1 = new Node("n1");        Node n2 = new Node("n2");        Node n3 = new Node("n3");        // 构造一个单向链表        n1.next = n2;        n2.next = n3;        System.out.println(n1);        // 插入一个元素 n4,放在 n1 和 n2 之间        Node n4 = new Node("n4");        n1.next = n4;        n4.next = n2;        System.out.println(n1);        // 删除元素 n2        n4.next = n3;        System.out.println(n1);    }}class Node {    public String data; // 存放的元素    public Node next;   // 指向下一个节点的引用    public Node(String data) {        super();        this.data = data;    }    @Override    public String toString() {        return "Node [data=" + data + ", next=" + next + "]";    }}

控制台输出结果:

这里写图片描述

这就是单向链表,至于循环链表,就是把最后一个元素指向第一个元素,下面我们再来看下双向循环链表的实现。

双向循环链表的实现

这里写图片描述

package com.zhou.collection_11;public class DoubleLoopLinkedListDemo {    public static void main(String[] args) {        Node1 n1 = new Node1("n1");        Node1 n2 = new Node1("n2");        Node1 n3 = new Node1("n3");        // 构造一个双向循环链表        n1.next = n2;        n1.previous = n3;        n2.next = n3;        n2.previous = n1;        n3.next = n1;        n3.previous = n2;        // 插入一个元素 n4,放在 n1 和 n2 之间        Node1 n4 = new Node1("n4");        n1.next = n4;        n4.previous = n1;        n4.next = n2;        n2.previous = n4;        // 删除元素 n2        n4.next = n3;        n3.previous = n4;        n2.next = null;        n2.previous = null;    }}class Node1 {    public Node1 previous; // 指向前一个节点,前驱    public Node1 next;     // 指向下一个节点,后继    public String data;    // 存放数据    public Node1(String data) {        super();        this.data = data;    }}

这就是双向循环链表的实现,我们不能通过复写toString()方法来把所以元素打印出来,因为链表本身是一个死循环。既然我们明白的链表的实现原理,那么LinkedList是否也是类似,去追踪一下LinkedList实现源码。

4.源码实现分析

全局变量

     transient int size = 0;    /**     * Pointer to first node.     * Invariant: (first == null && last == null) ||     *            (first.prev == null && first.item != null)     */    transient 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就是指向最后一个元素。在来看下Node< E >这个类的实现。

  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;        }    }

它是一个私有的内部类,里面就是一个泛型类型的变量item,指向前一个元素的变量prev,指向后一个元素的变量next。其中泛型item就是我们向LinkedList中添加的元素,然后Node又构造好了向前与向后的引用prev,next,最后将生成的这个Node对象加入到了链表当中。这跟我们前面实现的双向链表循环链表是一样的结构,换句话说,LinkedList中所维护的是一个个的Node对象。

构造方法

    /**     * 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);    }

只有2个构造方法,一个无参的空实现,一个是传入集合来构造有大小的LinkedList,调用的addAll()方法,我们来看下这个方法的实现。

    public boolean addAll(Collection<? extends E> c) {       return addAll(size, 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;        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;    }

其实最终调用的是addAll(int index, Collection< ? extends E > c)方法,初始化的时候size为0,所以index是0。

checkPositionIndex(index);private void checkPositionIndex(int index) {        if (!isPositionIndex(index))            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));    } private boolean isPositionIndex(int index) {        return index >= 0 && index <= size;    }

首先通过checkPositionIndex()方法进行范围检查,大小超出范围抛出IndexOutOfBoundsException异常。

    Object[] a = c.toArray();    int numNew = a.length;    if (numNew == 0)        return false;

接下来将传入的集合转化为数组,如果大小为0,就直接返回。也就是如果传入的集合大小为0,也是构造一个空的LinkedList。

        Node<E> pred, succ;        if (index == size) {            succ = null;            pred = last;        } else {            succ = node(index);            pred = succ.prev;        }

初始化的size为0,传入的index也为0,变量last为null,所以走第一个判断条件,succ为null,pred也为null。

        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;        }

这段代码就是构造了整个linkedList的元素,first指向第一个元素,pred指向最后一个元素。

        if (succ == null) {            last = pred;        } else {            pred.next = succ;            succ.prev = pred;        }        size += numNew;        modCount++;        return true;

由于succ为null,把pred赋值给last,所以last也指向最后一个元素,再更改列表的大小。

添加/增加元素

添加元素主要就是add()方法和addAll()方法,在加上addFirst()和addLast()方法,下面我们就来看一下实现代码。

 public boolean add(E e) {        linkLast(e);        return true;    }  public void add(int index, E element) {        checkPositionIndex(index);        if (index == size)            linkLast(element);        else            linkBefore(element, node(index));    }public void addFirst(E e) {        linkFirst(e);    }public void addLast(E e) {        linkLast(e);    }

分析上面代码,主要就是linkLast()、linkBefore()、linkFirst()这几个方法来实现添加元素的。

    // 在链表的头部添加一个元素    private void linkFirst(E e) {        final Node<E> f = first;        // 构造一个前驱为null,后继为f的Node对象        final Node<E> newNode = new Node<>(null, e, f);        // 第一个元素指向刚刚构造出来的对象        first = newNode;        // 如果这个链表是空的,则第一个元素也是最后一个元素        // 否则把以前的前驱指向刚刚构造出的元素        if (f == null)            last = newNode;        else            f.prev = newNode;        // 列表大小+1        size++;        modCount++;    }

同理,其他的添加方法都是类似的,linkFirst()每次在头部添加元素,linkLast()每次在尾部添加元素,linkBefore()在任意位置添加元素。所以我们每次调用add(E e),都是在最后添加一个元素。这个其中有一个很重要的方法 Node< E > node(int 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小于size/2,把列表第一个元素提取出来赋值给x,如果需要的不是第一个元素,循环遍历它的后继元素,找到最终的x。

如果index大于size/2,取出最后一个元素赋值给x,如果需要的不是最后一个元素,循环遍历它的前驱元素,找到最终的x。

可想而知,这个方法应该是贯穿整个LinkedList的方法。增删改查肯定都需要调用到。

查找元素

主要的查询方法有:getFirst()、getLast()、get()

    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 get(int index) {        checkElementIndex(index);        return node(index).item;    }

查询方法很简单,对于getFirst()、getLast()来说,直接返回头元素和尾元素的数据,如果为空则抛出一个NoSuchElementException异常。

get()方法则直接调用了node()方法,所以它查询元素的效率高的原因我们也就知道了。

修改元素

可以说List集合都是通过set()方法来修改元素的。

  public E set(int index, E element) {        checkElementIndex(index);        Node<E> x = node(index);        E oldVal = x.item;        x.item = element;        return oldVal;    }

就是通过node()方法找到指定位置的元素,修改其中的item。

删除元素

是通过remove()、remove(Object o)、remove(int index)方法来删除元素的。

    public E remove() {        return removeFirst();    }    public E remove(int index) {        checkElementIndex(index);        return unlink(node(index));    }    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;    }

通过代码发现删除主要调用的还是E unlink(Node< E > x)方法,下面我们来看下unlink()的实现。

    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的后继后继赋值给first,x的后继就指向null,x的前驱也指向null,x后继的前驱指向前驱(null),这样就把x删除了。

如果传入的元素x为最后一个元素的话,则说明要删除的是最后一个元素,则把x的前驱赋值给last,x的后继就指向null,x的前驱也指向null,x前驱的后继指向后继(null),这样就把x删除了。

如果删除非头尾元素,则把x的后继就指向null,x的前驱也指向null,x前驱的后继指向后继,x后继的前驱指向前驱,这样就把x删除了。

可能有点绕口,语言表达能力有限,需要自己体会一下。

这里写图片描述

Node< E > node(int index)

其实LinkedList的主要操作都在这个几个方法中,已经分析了几个主要的方法,其他的实现都很简单,跟我们上面双向循环链表的Demo都很相似,只是它属于双向链表,不是循环的而已。它们大部分都是私有的,外部不可调用,直接操作Node对象里面的数据的。下面这是几个方法的实现,可以看下:

    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++;    }    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++;    }    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++;    }    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;    }    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;    }

5.其他方法的分析

这里写图片描述

通过源码,你会发现,这些方法的实现,都是通过上面讲的一个first变量、一个last变量,加上上面的几个重要的操作Node数据的方法,这些变量和方法来实现的。例如peek()方法,判断first是否为null,是返回null,不是返回first.item对象。

    public E peek() {        final Node<E> f = first;        return (f == null) ? null : f.item;    }

我们来看下clear()和toArray()方法。

    public void clear() {        // Clearing all of the links between nodes is "unnecessary", but:        // - helps a generational GC if the discarded nodes inhabit        //   more than one generation        // - is sure to free memory even if there is a reachable Iterator        for (Node<E> x = first; x != null; ) {            Node<E> next = x.next;            x.item = null;            x.next = null;            x.prev = null;            x = next;        }        first = last = null;        size = 0;        modCount++;    }

这是一个所以变量置空的操作,这样可以被gc回收,很有代表性。我们以后写的代码,当销毁对象时,也要有这样的清除操作。循环置空,避免内存泄漏。

    public Object[] toArray() {        Object[] result = new Object[size];        int i = 0;        for (Node<E> x = first; x != null; x = x.next)            result[i++] = x.item;        return result;    }

LinkedList的toArray()方法和ArrayList的不一样哦~原来构造出一个数组,把元素一个一个的添加进去,没有用到Arrays类的方法。

6.和ArrayList的比较分析

1.ArrayList底层是采用数组实现的,而LinkedList底层采用双向链表实现的。

2.当执行插入或者删除操作时,采用LinkedList比较好。

3.当执行搜索操作时,采用ArrayList比较好。

4.对于ArrayList的删除元素操作,需要将删除元素的后续元素,整体向前移动,所以代价比较高。

5.集合中只能放置对象的引用,无法放置原生的数据类型。我们需要使用原生数据类型的包装类才能加到集合中去。集合中放置的对象都是Object类型的,因此取出来的也都是Object类型的,那么必须使用强制类型转化将其转化为真正的类型。

每个人的见解和分析都有可能不同,强烈建议自己可以在看下源码,然后我们交流讨论。

1 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 宝宝不愿意盖被子怎么办 白天睡觉晚上睡不着怎么办 晚上睡觉不安神怎么办 晚上经常睡不着觉怎么办 老是睡不着觉怎么办啊 小孩睡觉不安稳怎么办 睡觉时动不了怎么办 特别累还睡不着怎么办 又累又睡不着怎么办 干活累的睡不着怎么办 狗一有动静就叫怎么办 楼上天天闹动静怎么办 喝了奶茶失眠怎么办 失眠一宿第二天怎么办 睡觉外面噪音大怎么办 怀孕早期晚上睡不着怎么办 短发发尾翘怎么办 很累就是睡不着怎么办 人累但是睡不着怎么办 如果晚上睡不着该怎么办 晚上睡不着觉该怎么办 晚上睡不着该怎么办呢 晚上失眠睡不着该怎么办 晚上一直睡不着该怎么办 怀孕晚上睡不着该怎么办 运动太累睡不着怎么办 运动完睡不着觉怎么办 晚上冷得睡不着怎么办 晚上脚冷睡不着怎么办 短发头发有点乱怎么办 不想让别人睡觉怎么办 15岁晚上睡不着怎么办 16岁青少年失眠怎么办 好累又睡不着怎么办 造口患者拉肚子怎么办? 起床后头发乱怎么办 新生儿睡觉偏头怎么办 婴儿睡觉偏头怎么办 月经期间血下不来怎么办 月经下不来怎么办一点点咖啡色 突然早睡睡不着怎么办