JAVA常用集合源码分析:LinkedList

概述

上一篇我们介绍了ArrayList,我们知道它的底层是基于数组实现的,提到数组,我们就马上会想到它的兄弟链表,今天我们要介绍的LinkedList就是基于链表实现的。

继承结构

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

List接口:列表,add、set、等一些对列表进行操作的方法
Deque接口:有队列的各种特性,可以当队列用
Cloneable接口:能够复制,使用那个copy方法。
Serializable接口:能够序列化。
注意:并没有实现RandomAccess:那么就推荐使用iterator,在其中就有一个foreach,增强的for循环,其中原理也就是iterator,我们在使用的时候,使用foreach或者iterator都可以。 

源码分析

先看有哪些成员变量

//元素个数
transient int size = 0;
//首节点
transient Node<E> first;
//尾节点
transient Node<E> last;

在这里,我们发现first和last都是Node类型的,我们不难想到这是节点类

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

根据源码,我们可以发现节点有三个属性,分别是值和指向一前一后的两个指针

《JAVA常用集合源码分析:LinkedList》

看到这里,我们可以得出结论,LinkedList是底层是一个双向链表,但究竟是不是双向循环链表呢?我们还得继续往下看。

有两个构造方法

//空实现 
public LinkedList() {
    }

//用一个集合构建
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

第二个构造方法中,出现了addAll(e)方法,显然该方法实现了把c中所以元素加到一个空链表中,我们来看看它的具体实现

public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }

只有短短两行,主要调用了我们的addAll(int, E)方法,继续看它的实现

public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index); //检查边界

        Object[] a = c.toArray(); // 把c里的值转化为数组
        int numNew = a.length;
        if (numNew == 0)  //如果c中无元素,则增加失败
            return false;

        Node<E> pred, succ;  // 两个指针,一个指前,一个后

        if (index == size) { //注意我们构造函数中一开始传进来的就是size,所以会进入
            succ = null;
            pred = last;
        } else { //当我们传入的不是size的时候,进入这里
            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;
    }

代码有点长,但是不难,容易看懂,同时在这个方法中,我们也可以知道底层只是个双向链表,并非是双向循环链表啦,事实上JDK1.7之前一直是循环链表来着,至于为啥要改,我也不敢妄下定论….

好啦,分析完了构造函数,我们接着看他一些常用的方法的实现把

常用方法

1.add(E)

《JAVA常用集合源码分析:LinkedList》

public boolean add(E e) {
        linkLast(e); //默认是在末尾增加
        return true;
    }
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++;
    }

哈哈,代码不难,一看就懂

2.add(int , e) 在指定位置增加元素

《JAVA常用集合源码分析:LinkedList》

 public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size) //如果刚好是在最后一个增加,就直接加啦
            linkLast(element);
        else
            linkBefore(element, node(index));  //在中间加
    }

//在中间加的时候利用一分为二思想,看index离头近还是离尾近
Node<E> node(int index) {

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

 具体的思路我都写在注释上啦,在node(index)的时候,运用了一分为二思想,结合了双向链表的优点,有点巧妙

3.remove(Object) 删除第一次的Object

《JAVA常用集合源码分析:LinkedList》

其实思路不难,就是先找到出现的位置,然后执行删除操作

 public boolean remove(Object o) {
        if (o == null) {  // 区分是否为空,因为空值无法执行 equals方法
            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;
    }

核心操作就是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就行啦
            first = next;
        } else {            //前继指向它的后继,相当于跳过它啦
            prev.next = next;
            x.prev = null;  //置空
        }

        if (next == null) {
            last = prev;   // 太简单,跳过
        } else {
            next.prev = prev;   //next的前继指向 原节点的前继。
            x.next = null;
        }

        x.item = null;  //置空,让GC回收它
        size--;
        modCount++;
        return element;
    }

可以结合上面的图来理解

4.remove(index)  删除给定位置的对象啦

其实原理都差不多,首先是定位,再删除…注意删除的点是头或者尾节点这种特殊情况就行啦

5.get(index) 

public E get(int index) {
        checkElementIndex(index); //检查边界
        return node(index).item;
    }

核心定位代码node(index)之前已经介绍过啦,虽然进行了一些优化,性能已经变为了O(n/2),但还是非常低效的。

总结

  • LinkedList 插入,删除都是移动指针效率很高。
  • 查找需要进行遍历查询,效率较低

与ArrayList的比较

  • ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
  • ArrayList 支持随机访问,LinkedList 不支持;
  • LinkedList 在任意位置添加删除元素更快。

想说的话

哈哈第二次看源码啦,虽然速度慢,但感觉还是不错的啦,分析源码的思路感觉更加顺畅了些。有些观点参考了其他的博客,在浏览其他博客的时候,也有发现其他博客说的不够准确的地方,也正是这样,让我感觉博客必须要严谨些,一定要尽量多了解些,不然对其他初学者产生勿扰就糟糕啦哈哈哈哈

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/qq_37410328/article/details/82892894
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞