Java集合之LinkedList源码解析

概述

b78ac625174a4111b9aa632a00a5fa41.png

  • 是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的。
  • 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
  • 实现 List 接口,能对它进行队列操作。
  • 实现 Deque 接口(带可),即能将LinkedList当作双端队列使用。
  • 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  • 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
  • 是非同步,线程不安全的。

数据结构

bb7c94ea1ad54cab92e7954346cfb7e7.png

 如上图所示,LinkedList底层使用的双向链表结构,有一个头结点和一个尾结点,双向链表意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。

特性

1)异步,也就是非线程安全

2)双向链表。由于实现了list和Deque接口,能够当作队列来使用。

链表:查询效率不高,但是插入和删除这种操作性能好。

3)是顺序存取结构

源码分析

类的属性

​// 实际元素个数,链表长度transient int size = 0;
// 头结点transient Node<E> first;
// 尾结点transient Node<E> last;

LinkedList的属性非常简单,一个头结点、一个尾结点、一个表示链表中实际元素个数的变量。

构造方法

两个构造方法(两个构造方法都是规范规定需要写的)

1)空参构造函数

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

2)有参构造函数

//将集合c中的各个元素构建成LinkedList链表。public LinkedList(Collection<? extends E> c) {
    // 调用无参构造函数
    this();
    // 添加集合中所有的元素
    addAll(c);
}

说明:会调用无参构造函数,并且会把集合中所有的元素添加到LinkedList中。

内部类(Node)

//根据前面介绍双向链表就知道这个代表什么了,linkedList的奥秘就在这里。

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

说明:内部类Node就是实际的结点,用于存放实际元素的地方。

核心方法

1 add()方法

8609d0e373d7419f9c45f00dfced33ba.png

私有方法

LinkedList 内部有几个关键的私有方法,它们实现了链表的插入、删除等操作。比如在表头插入:

private void linkFirst(E e) {
    final Node<E> f = first;    //先保存当前头节点
    //创建一个新节点,节点值为e,前驱节点为空,后继节点为当前头节点
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;    //让first指向新节点
    if (f == null)    //如果链表原来为空,把last指向这个唯一的节点
        last = newNode;
    else    ·        //否则原来的头节点的前驱指向新的头节点
        f.prev = newNode;
    size++;
    modCount++;
}
其实就是双向链表的插入操作,调整指针的指向,时间复杂度为 O(1) ,学过数据结构的应该很容易看懂。其它还有几个类似的方法://尾部插入void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)    //如果链表原来为空,让first指向这个唯一的节点
        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;    //让first指向下一个节点
    if (next == null)    //如果下一个节点为空,说明链表原来只有一个节点,现在成空链表了,要把last指向null
        last = null;
    else        //否则下一个节点的前驱节点要置为null
        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;    //last指向前一个节点
    if (prev == null)    //与头节点删除一样,判断是否为空
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}//从链表中间删除节点
 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要指向下一个节点
        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;
}

1)add(E)

说明:add函数用于向LinkedList中添加一个元素,并且添加到链表尾部。具体添加到尾部的逻辑是由linkLast函数完成的。

public boolean add(E e) {
    // 添加到末尾
    linkLast(e);
    return true;
}

LinkLast()//尾部插入

/**
     * Links e as last element.
     */void linkLast(E e) {
    final Node<E> l = last;    //临时节点l(L的小写)保存last,也就是l指向了最后一个节点
    final Node<E> 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++;
}

举例:

 

List<Integer> lists = new LinkedList<Integer>();

lists.add("e1");

lists.add("e2");

一开始,first和last都为null,此时链表什么都没有,当第一次调用该方法后,first和last均指向了第一个新加的节点E1:

74423e397c2343e3b67eb40055749e70.png 

第二次调用该方法,加入新节点E2。首先,将last引用赋值给l,接着new了一个新节点E2,并且E2的prve指向l,接着将新节点E2赋值为last

e6bbe093e6574234ad642994b580c7c5.png

接着判断l==null? 所以走的else语句,将l的next引用指向新节点E2

39a5d5d58fb94c7294758558ce9096eb.png

最后,size+1,modCount+1,退出该方法,局部变量l销毁

1c7dc96ce2b44cff84519967451f7990.png

公开的方法几乎都是调用上面几个方法实现的,例如 add 方法:

public boolean add(E e) {
    linkLast(e);
    return true;
}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));
}

add(int index, E element)的逻辑稍显复杂,可以分成两部,1.先根据index找到要插入的位置;2.修改引用,完成插入操作。

//add(int index, E element)public void add(int index, E element) {
    checkPositionIndex(index);//index >= 0 && index <= size;
    if (index == size)//插入位置是末尾,包括列表为空的情况
        add(element);
    else{
         Node<E> succ = node(index);//1.先根据index找到要插入的位置
        //2.修改引用,完成插入操作。
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)//插入位置为0
            first = newNode;
        else
            pred.next = newNode;
        size++;
    }
}

上面代码中的node(int index)函数有一点小小的trick,因为链表双向的,可以从开始往后找,也可以从结尾往前找,具体朝那个方向找取决于条件index < (size >> 1),也即是index是靠近前端还是后端。

2).remove()方法也有两个版本,一个是删除跟指定元素相等的第一个元素remove(Object o),另一个是删除指定下标处的元素remove(int index)。

remove(int index)
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

remove(Object o)

//首先通过看上面的注释,我们可以知道,如果我们要移除的值在链表中存在多个一样的值,那么我们会移除index最小的那个,也就是最先找到的那个值,如果不存在这个值,那么什么也不做public boolean remove(Object o) {
    //这里可以看到,linkedList也能存储null
    if (o == null) {
        //循环遍历链表,直到找到null值,然后使用unlink移除该值。下面的这个else中也一样
        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;
}

 第一个 remove(int index) 方法同样要调用 node(index) 寻找节点。而第二个方法 remove(Object o) 是删除指定元素,这个方法要依次遍历节点进行元素的比较,最坏情况下要比较到最后一个元素,比调用 node 方法更慢,时间复杂度为 O(n) 。另外从这个方法可以看出 LinkedList 的元素可以是 null 。

3). get(index)

get(index)查询元素的方法


//这里没有什么,重点还是在node(index)中public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

node(index)


/**
     * Returns the (non-null) Node at the specified element index.
     *///这里查询使用的是先从中间分一半查找
    Node<E> node(int index) {
        // assert isElementIndex(index);//"<<":*2的几次方 “>>”:/2的几次方,例如:size<<1:size*2的1次方,//这个if中就是查询前半部分
         if (index < (size >> 1)) {//index<size/2
            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;
        }
    }

 4).set(int index, E element)方法将指定下标处的元素修改成指定值,也是先通过node(int index)找到对应下表元素的引用,然后修改Node中item的值。

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

5). indexOf(Object o)


//这个很简单,就是通过实体元素来查找到该元素在链表中的位置。跟remove中的代码类似,只是返回类型不一样。public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

总结

1)linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构。

 2)能存储null值

 3)跟arrayList相比较,就真正的知道了,LinkedList在删除和增加等操作上性能好,而ArrayList在查询的性能上好 

4)从源码中看,它不存在容量不足的情况

5)linkedList不光能够向前迭代,还能像后迭代并且在迭代的过程中,可以修改值、添加值、还能移除值。

 6)linkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。

 

  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值