JAVA集合源码分析——LinkedList

一、LinkedList概述

1)linkedList是基于双向链表实现的有序序列,对插入和删除具备高效率
2)linkedList继承AbstractSequentialList的双向链表
3)实现了List接口,可以实现队列操作
4)实现了Deque接口,能被当做双端队列使用
5)实现了Cloneable接口,能调用Object.clone()方法
6)实现了Serializable接口,支持序列化,非同步

二、LinkedList特性

1)底层数据结构是双向链表
2)插入和删除数据的效率高
3)查询数据时,需要遍历链表来靠近所查值的index来获取数据,即顺序存取
4)异步,即线程不安全

三、LinkedList源码分析

1、LinkedList继承结构和实现接口分析
JAVA集合源码分析——LinkedList_第1张图片分析:
1)LinkedList通过实现AbstractSequentialList抽象类,从而具备了顺序存取的特性;相反,如果想要随机存取那么就实现AbstractList抽象类

JAVA集合源码分析——LinkedList_第2张图片分析:
1)实现了List接口,具备了对列表进行操作的方法
2)Deque接口,具备了队列的特性
3)Cloneable接口,能使用clone()方法
4)Serializable接口,能序列化
5)由于没有实现RandomAccess接口,所以推荐使用迭代器迭代比遍历效率好

四、LinkedList源码分析

1、linkedList类属性
JAVA集合源码分析——LinkedList_第3张图片
分析:
1)transient int size = 0//linkedList链表中实际元素个数,不会被序列化
2)transient Node first;//linkedList链表的头指针,序列化时该域不会被序列化
3)transient Node last;//linkedList链表的尾指针,不会被序列化

2、LinkedList的构造方法
无参构造函数:
JAVA集合源码分析——LinkedList_第4张图片有参构造函数:
JAVA集合源码分析——LinkedList_第5张图片说明:有参构造函数会调用无参构造函数,之后会把参数集合里的元素加入进链表中

3、关键数据结构和方法(node())
Node内部类
JAVA集合源码分析——LinkedList_第6张图片分析:
1)E item;//节点值
2)Node next;//节点的后指针
3)Node prev;//节点的前指针
4)构造函数就是把元素构造为Node对象

node()方法解析:

    Node node(int 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; // 返回该结点
    }
}

说明:node(index)方法采用的是遍历但是只遍历前半段或者后半段而不是整段的遍历

4、LinkedList典型方法分析
1)add(E)默认吧元素添加到链表末尾
JAVA集合源码分析——LinkedList_第7张图片add方法默认吧元素添加到链表的尾部,而底层调用的是linkLast(e)方法
分析:linkedlast(e)

void linkLast(E e) {
    final Node l = last;    //临时节点l(L的小写)保存last,也就是l指向了最后一个节点
    final Node newNode = new Node<>(l, e, null);//将e封装为节点,并且e.prev指向了最后一个节点
    last = newNode;//newNode成为了最后一个节点,所以last指向了它
    if (l == null)    //判断是不是一开始链表中就什么都没有,如果没有,则newNode就成为了第一个节点,first和last都要指向它
        first = newNode;
    else    //正常的在最后一个节点后追加,那么原先的最后一个节点的next就要指向现在真正的最后一个节点,原先的最后一个节点就变成了倒数第二个节点
        l.next = newNode;
    size++;//添加一个节点,size自增
    modCount++;
}

2)addAll
addAll有两个重载函数,addAll(Collection)型和addAll(int, Collection)型,我们平时习惯调用的addAll(Collection)型会转化为addAll(int, Collection)型。

1、addAll(collection);//把集合中的元素添加到链表末尾
JAVA集合源码分析——LinkedList_第8张图片说明:addAll(collection)方法的内部会调用addAll(index,collection)方法

分析:addAll(index,coll)

真正核心的地方就是这里了,记得我们传过来的是size,c
public boolean addAll(int index, Collection c) {
    检查index这个是否为合理。
    checkPositionIndex(index);
    //将集合c转换为Object数组 a
    Object[] a = c.toArray();
    //数组a的长度numNew,也就是由多少个元素
    int numNew = a.length;
    if (numNew == 0)
    //集合c是个空的,直接返回false,什么也不做。
        return false;
        //集合c是非空的,定义两个节点(内部类),每个节点都有三个属性,item、next、prev。注意:不要管这两个什么含义,就是用来做临时存储节点的。这个Node看下面一步的源码分析,Node就是linkedList的最核心的实现,可以直接先跳下一个去看Node的分析
    Node pred, succ;
    //构造方法中传过来的就是index==size
    if (index == size) {
    //linkedList中三个属性:size、first、last。 size:链表中的元素个数。 first:头节点  last:尾节点,就两种情况能进来这里
    //情况一、:构造方法创建的一个空的链表,那么size=0,last、和first都为null。linkedList中是空的。什么节点都没有。succ=null、pred=last=null
    //情况二、:链表中有节点,size就不是为0,first和last都分别指向第一个节点,和最后一个节点,在最后一个节点之后追加元素,就得记录一下最后一个节点是什么,所以把last保存到pred临时节点中。
        succ = null;
        pred = last;
    } else {
    //情况三、index!=size,说明不是前面两种情况,而是在链表中间插入元素,那么就得知道index上的节点是谁,保存到succ临时节点中,然后将succ的前一个节点保存到pred中,这样保存了这两个节点,就能够准确的插入节点了
    //举个简单的例子,有2个位置,1、2、如果想插数据到第二个位置,双向链表中,就需要知道第一个位置是谁,原位置也就是第二个位置上是谁,然后才能将自己插到第二个位置上。如果这里还不明白,先看一下文章开头对于各种链表的删除,add操作是怎么实现的。
        succ = node(index);
        pred = succ.prev;
    }
    //前面的准备工作做完了,将遍历数组a中的元素,封装为一个个节点。
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //pred就是之前所构建好的,可能为null、也可能不为null,为null的话就是属于情况一、不为null则可能是情况二、或者情况三
        Node newNode = new Node<>(pred, e, null);
        //如果pred==null,说明是情况一,构造方法,是刚创建的一个空链表,此时的newNode就当作第一个节点,所以把newNode给first头节点
        if (pred == null)
            first = newNode;
        else
        //如果pred!=null,说明可能是情况2或者情况3,如果是情况2,pred就是last,那么在最后一个节点之后追加到newNode,如果是情况3,在中间插入,pred为原index节点之前的一个节点,将它的next指向插入的节点,也是对的
            pred.next = newNode;
            //然后将pred换成newNode,注意,这个不在else之中,请看清楚了。
        pred = newNode;
    }
    if (succ == null) {
    //如果succ==null,说明是情况一或者情况二,
    //情况一、构造方法,也就是刚创建的一个空链表,pred已经是newNode了,last=newNode,所以linkedList的first、last都指向第一个节点。
    情况二、在最后节后之后追加节点,那么原先的last就应该指向现在的最后一个节点了,就是newNode。
        last = pred;
    } else {
    //如果succ!=null,说明可能是情况三、在中间插入节点,举例说明这几个参数的意义,有1、2两个节点,现在想在第二个位置插入节点newNode,根据前面的代码,pred=newNode,succ=2,并且1.next=newNode,
    1已经构建好了,pred.next=succ,相当于在newNode.next = 2; succ.prev = pred,相当于 2.prev = newNode, 这样一来,这种指向关系就完成了。first和last不用变,因为头节点和尾节点没变
        pred.next = succ;
        succ.prev = pred;
    }
    //增加了几个元素,就把 size = size +numNew 就可以了
    size += numNew;
    modCount++;
    return true;
}

小结:其实在addAll(index,col)方法内部它要考虑的情况如下:
1、添加元素的位置
判断条件:
if(index == size),该判断成立就说明是待插入的元素的插入链表的位置在末尾,不成立就表示不在末尾
1)在链表末尾
2)不在链表末尾

2、链表是否为空
对于内部的两个变量节点指针:pred、succ,的取值是基于链表是否为空和元素插入位置共同确定
1)链表是为空,且插入位置在链表末尾或者不在末尾
pred = last = null
succ = null
2)链表不为空,且插入位置在链表末尾
pred = last
succ = null
3)链表不为空,且插入位置不在末尾
succ = node(index)//succ指向待插入位置处对应的原节点,即succ指向插入位置
pred = succ.prev;//pred指向插入节点位置原节点的前节点;即pred指向插入位置的前一个位置
在考虑完上述的所有情况后,addAll(index,coll)才开始真的进行插入
JAVA集合源码分析——LinkedList_第9张图片这一段代码就是,把带插入的数据变成linkedList内部的节点对象之后,其中每个节点的前指针均指向pred指针所指向的对象,即指向待插入节点处的前一个节点;而后指针初始化为null即把每个节点暂时变成尾节点。之后便开始依据上述的三种情况来设置linkedList的头指针:
if(pred == null)如果成立就说明链表为空,那么透指针就要指向第一个插入的节点处:first = newNode;
之后便依据:
pred.next = newNode;
pred = newNode;
这两句代码吧插入的节点的后指针和第一个插入节点与插入位置原节点的前节点串起来成为一个完整的双向链表
之后便是依据上述的三种情况来设置最后一个插入链表中节点的尾指针的指向和链表的尾指针是否要改变
JAVA集合源码分析——LinkedList_第10张图片

  1. if(succ == null)//成立就代表,链表初始状态是空链表或者链表不是空链表时但插入元素的位置在末尾
    那么最后一个节点的尾指针就是null,此时不用修改,只需改变链表的尾指针last指向插入的最后一个节点,使其成为尾节点
    2)如果succ != null那么就说明插入元素的节点在链表中间,那么就要设置插入的最后一个元素的尾节点:
    pred.next = succ;//其后指针指向插入位置处的原节点
    succ.prev = pred;//插入位置处的原节点的前指针指向最后一个插入元素
    链表尾指针不用改变

3)remove(Object o)//删除指定元素

 * Removes the first occurrence of the specified element from this list,
 * if it is present.  If this list does not contain the element, it is
 * unchanged.  More formally, removes the element with the lowest index
 * {@code i} such that
 * (o==null ? get(i)==null : o.equals(get(i)))
 * (if such an element exists).  Returns {@code true} if this list
 * contained the specified element (or equivalently, if this list
 * changed as a result of the call).
 *
 * @param o element to be removed from this list, if present
 * @return {@code true} if this list contained the specified element
 首先通过看上面的注释,我们可以知道,如果我们要移除的值在链表中存在多个一样的值,那么我们会移除index最小的那个,也就是最先找到的那个值,如果不存在这个值,那么什么也不做
public boolean remove(Object o) {
//这里可以看到,linkedList也能存储null
    if (o == null) {
    //循环遍历链表,直到找到null值,然后使用unlink移除该值。下面的这个else中也一样
        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;
}

分析unlink(node):

  Unlinks non-null node x.
  //不能传一个null值过,注意,看之前要注意之前的next、prev这些都是谁。
   E unlink(Node x) {
    // assert x != null;
    //拿到节点x的三个属性
    final E element = x.item;
    final Node next = x.next;
    final Node prev = x.prev;
    //这里开始往下就进行移除该元素之后的操作,也就是把指向哪个节点搞定。
    if (prev == null) {
    //说明移除的节点是头节点,则first头节点应该指向下一个节点
        first = next;
    } else {
    //不是头节点,prev.next=next:有1、2、3,将1.next指向3
        prev.next = next;
        //然后解除x节点的前指向。
        x.prev = null;
    }
      if (next == null) {
    //说明移除的节点是尾节点
        last = prev;
    } else {
    //不是尾节点,有1、2、3,将3.prev指向1. 然后将2.next=解除指向。
        next.prev = prev;
        x.next = null;
    }
    //x的前后指向都为null了,也把item为null,让gc回收它
    x.item = null;
    size--;    //移除一个节点,size自减
    modCount++;
    return element;    //由于一开始已经保存了x的值到element,所以返回。
}

3)get(index)
JAVA集合源码分析——LinkedList_第11张图片
说明:get(index)内部还是依据node(index)及遍历链表的前半段或者后半段来靠近index取得index上的节点值

4)indexOf(Object o)
JAVA集合源码分析——LinkedList_第12张图片
说明:可以看出indexOf是采用的遍历整个链表的值来匹配所要查询的值,同时linkedList还支持存放null,不存在对应的值时放回-1

5、总结
1)linkedList底层数据结构是双向链表,插入和删除数据效率快,查询数据依据的是顺序取即遍历
2)注意node(index)关键方法、Node(prev,E,next)数据结构、addAll(index,col)函数实现思想即对链表的操作
3)清楚linkedList内部的属性意义

你可能感兴趣的:(LinkedList源码分析)