• LinkedList源码解析


    LinkedList源码解析

    源码基于java8

    LinkedList整体结构

    在这里插入图片描述

    LinkedList是实现了List接口和Deque接口,他的结构类似于双端链表。
    实现Cloneable接口表示节点可以被浅拷贝,实现了Serializable接口代表可被序列化。
    LinkedList是线程不安全的,如果想变成线程安全的可以使用Collections中的
    synchronizedList方法。

    我们可以先看一下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;
            }
        }
    

    从源码中我们知道,链表中的每个节点称为Node,Node都有pre和next属性。

    
    //默认链表大小是0
    transient int size = 0;
    
        /**
         * 指向第一个Node
         * first必须满足他的prev是null并且他自身不是null
         *
         */
        transient Node<E> first;
    
        /**
            last的next是null,并且自身不是null
         */
        transient Node<E> last;
    
        /**
         * 构造方法
         */
        public LinkedList() {
        }
    

    当链表中没有数据时,first和last是同一个节点,前后都指向null。

    所以可以结构图可以是这样(自己画的比较丑,别介意)
    在这里插入图片描述
    ps: 在Java8中已经不是循环链表了,只是双向链表。

    新增

    追加节点时,可以从头部追加也可以新增到链表尾部,默认是追加到链表尾部,add方法是追加尾部,addFirst是从头部开始追加。

    先看add方法

    
    public boolean add(E e) {
            linkLast(e);
            return true;
        }
    //追加在链表最后
    void linkLast(E e) {
    //last是最后一个节点
            final Node<E> l = last;
            //因为追加到链表最后所以新节点的next=null
            final Node<E> newNode = new Node<>(l, e, null);
            //用新节点替换掉旧的last节点
            last = newNode;
            //旧的last是null,代表是第一次添加,所以新节点就是first
            if (l == null)
                first = newNode;
            else
            //否则,last.next=newNode
                l.next = newNode;
            //最后链表长度自增1,修改版本数加1
            size++;
            modCount++;
        }
    
    

    从头部追加(addFirst方法)

    //在链表头部增加
    public void addFirst(E e) {
            linkFirst(e);
        }
    //连接新元素,新元素作为first
    private void linkFirst(E e) {
    //首选记录下,上次的first
            final Node<E> f = first;
            //新建一个节点,因为追加在链表头,所以前驱是null,后继是上次的first
            final Node<E> newNode = new Node<>(null, e, f);
            //然后新节点作为现在的first
            first = newNode;
            //如果是第一次增加在链头,那么就是last节点
            if (f == null)
                last = newNode;
            else
            //否则旧的first的前驱节点就是当前新增的节点
                f.prev = newNode;
            //链表长度++,版本++
            size++;
            modCount++;
        }
    
    
    

    删除

    链表的节点删除和新增方式类似,可从尾部删除也可以从头部删除,删除会把节点的值前后节点都设置成null,方便GC回收。

    根据节点值删除

    
     public boolean remove(Object o) {
     //删除的节点是null
            if (o == null) {
            //从头结点开始循环遍历,直到遇到第一个节点值是null的,删除
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null) {
                    //删除并返回true
                        unlink(x);
                        return true;
                    }
                }
            } else {
            //如果要删除的元素不是null,还是需要一次遍历节点
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item)) {
                    //通过equals方法判断是不是将要删除的节点
                        unlink(x);
                        return true;
                    }
                }
            }
            //如果将要删除的节点不在链表中会返回false
            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;
            } else {
            //将前驱节点的后继指向  当前被删除节点的后继节点
                prev.next = next;
                //然后将被删除的节点的前驱置为null,有助于GC更快回收该对象
                x.prev = null;
            }
            //删除后继节点,如果后继节点是null,那代表是最后一个节点
            if (next == null) {
            //所以删除后,最后一个节点就是被删除节点的前驱
                last = prev;
            } else {
            //如果被删除的节点的后继不是null,那么后继节点的前驱就是被删除节点的前驱
                next.prev = prev;
                //最后需要将被删除的节点的后继指针指向null,也是帮助GC
                x.next = null;
            }
            //最后将被删除节点设置为null,链表长度--,版本号++。
            x.item = null;
            size--;
            modCount++;
            //返回待删除的元素
            return element;
        }
    

    删除指定位置的节点

    
    public E remove(int index) {
            //因为根据下标删除,所以需要检查一下是否发生越界
            checkElementIndex(index);
            return unlink(node(index));
        }
        
        //如果下标在这0~size就返回true,否则就会抛出 IndexOutOfBoundsException
        private boolean isElementIndex(int index) {
            return index >= 0 && index < size;
        }
        //检查下标是否越界
        private void checkElementIndex(int index) {
            if (!isElementIndex(index))
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    
    

    删除链表头部的节点

    
    //pop调用removeFirst,removeFirst调用unlinkFirst方法
    public E pop() {
            return removeFirst();
        }
    
    public E removeFirst() {
            final Node<E> f = first;
            //如果first不存在就不能删除,直接抛出NoSuchElementException异常
            if (f == null)
                throw new NoSuchElementException();
            return unlinkFirst(f);
        }
    
    //具体删除还得看unlinkFirst方法
    private E unlinkFirst(Node<E> f) {
            // assert f == first && f != null;
            //记录要删除的节点最终需要返回
            final E element = f.item;
            //first的后继节点
            final Node<E> next = f.next;
           //将被删除的节点的值置为null
           f.item = null;
           //这一步帮助GC
            f.next = null; 
            //然后被删除的节点的后继节点现在就成了,first节点
            first = next;
            //如果此时他为null,那就是个空链表
            if (next == null)
                last = null;
            else
            //否则的话,next的前驱需要指向null,因为first的前驱就是null,他将要变成first
                next.prev = null;
            
            //最终链表长度-1,版本号+1
            size--;
            modCount++;
            return element;
        }
    
    

    查询

    LinkedList查询节点的速度是比较慢的,需要挨个循环查找。

    根据链表索引位置查询节点

    //根据元素下标,返回非空节点
    Node<E> node(int index) {
            // assert isElementIndex(index);
            //如果index处于队列的牵绊部分,从头开始查找,size>>1=size/2
            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;
                //循环到index的后一个节点
                for (int i = size - 1; i > index; i--)
                    x = x.prev;
                //返回node
                return x;
            }
        }
    
    

    在查找的时候,LinkedList并没有采用从头到尾进行循环的方法,而是根据二分查找的思想,缩小了查找范围。如果在链表的前半部分就从前开始查,如果在后半部分流从后往前查,这样做提高了一些性能。

    因为LinkedList也实现了Deque接口,而Deque是继承自Queue接口的。
    所以实现了Queue的一些方法。

    这里进行简单的对比:

    • 新增 add offer;二者底层实现相同。
    • 删除 remove poll(e) 链表为空remove会抛出NoSuchElementException,poll会返回null
    • 查找 element peek 链表为null,element会抛出NoSuchElementException异常,peek返回null。

    总结

    ArrayList和LinkedList的对比

    1. 两者底层实现的数据结构不同。ArrayList底层是动态数组实现,LinkedList底层是双向链表。
    2. ArrayList和LinkedList都是不同步的,也就是不能保证线程安全。如果有线程安全问题,会抛出CouncurrentModificationException的错误,意思是在当前环境中,数组合链表的结构已经被其它线程所修改。所以 换成CopyOnWriteArrayList并发集合类使用或者Collection#synchronized。
    3. LinkedList不支持随机元素访问,ArrayList支持。因为ArrayList实现了RandomAccess接口,这个接口只是一个标识接口,接口中什么都没定义,相当于空接口,在binarySearch方法中,要判断传入的List集合是否是这个接口的实现,如果实现了RandomAccess接口才能使用indexedBinarySearch方法,否则只能一个个顺序遍历,也就是调用iteratorBinarySearch方法。
    4. 空间占用上LinkedList每次操作的对象就是Node节点,这个Node中有前驱和后继,而ArrayList的空间消耗主要是他在数组尾部会预留一定的空余,所以LinkedList的空间消耗比ArrayList更多。
    5. 再来看新增和删除。ArrayList顺序插入到数组尾部,时间复杂度是O(1),如果是指定位置插入或删除的话,时间复杂度是O(n-i);i是插入/删除的位置,n代表长度。LinkedList插入和删除操作的是节点的前驱和后继,所以直接改变指向,时间复杂度都是O(1)。LinkedList在做新增和删除的时候,慢在寻找被删除的元素,快在改变前后节点的引用地址。而ArrayList在新增和删除的时候慢在数组的copy,快在寻找被删除/新增的元素。

    以上有本人理解不到位的地方,欢迎各位指出,共同学习,共同进步!

  • 相关阅读:
    docker安装RabbitMQ
    通过Docker安装配置Mysql主从节点
    Docker基本使用命令
    flask接收post提交的json数据并保存至数据库
    前端面经
    js 仿朋友圈的时间显示 刚刚 几天前
    外部div宽度不是100%时,css设置图片宽高相等
    Vue项目图片剪切上传——vue-cropper的使用(二)
    Vue项目图片剪切上传——vue-cropper的使用
    vuex
  • 原文地址:https://www.cnblogs.com/dataoblogs/p/14121932.html
Copyright © 2020-2023  润新知