• LinkedList详解-源码分析


    LinkedList详解-源码分析

    LinkedList是List接口的第二个具体的实现类,第一个是ArrayList,前面一篇文章已经总结过了,下面我们来结合源码,学习LinkedList。

    • 基于双向链表实现

    • 便于插入和删除,不便于遍历

    • 非线程安全

    • 有序(链表维护顺序)

    • ...

    上面是LinkedList的一些特性。

    1. LinkedList类声明

    源码如下所示:

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

    初步分析:

    • 继承了AbstractSequentialList抽象类
    • 实现了List、Deque、Cloneable、Serializable接口

    思考:

    • List、Cloneable、Serializable接口在上一篇ArrayList详解里已经分析过了,这个Deque接口是干嘛的呢?

    咳咳,先谷歌一下,发现Deque的意思是双端队列,这里已经可以看出LinkedList是基于双向链表的一些端倪了,带着这点疑问,我们继续往下看。

    2. 成员变量

    源码如下所示:

        transient int size = 0;
    
        transient Node<E> first;
    
        transient Node<E> last;
    
    private static final long serialVersionUID = 876323262645176354L;
    

    比ArrayList的成员变量少了好几个呢。

    初步分析:

    • size依然是集合内的元素个数
    • transient关键字标识变量不会被序列化
    • Node是节点的意思,具体代码是什么样的?

    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是LinkedList的静态内部类,还是在LinkedList.class文件内部的

    分析:

    • 声明了三个成员变量
    • 分别表示当前元素,下一个节点,上一个节点

    也就是说,LinkedList的每一个元素都是一个Node,而每一个Node都储存了三部分内容,由此也就证实了LinkedList是基于双向链表的。

    3. 构造方法

    源码如下所示:

     	public LinkedList() {
        }
      
        public LinkedList(Collection<? extends E> c) {
            this();
            addAll(c);
        }
    

    分析:

    • LinkedList提供了两个构造方法
    • 分别是对应无参构造和传入Collection子类进行构造

    可以发现,相对于ArrayList,LinkedList类并没有指定容量的构造,这是为什么呢?

    思考:

    1. 这就是ArrayList和LinkedList底层依赖不同有关系,ArrayList底层是数组,LinkedList底层是双向链表。数组初始化是需要声明长度的,链表则不需要。

    2. 传入子类进行构造时,也是调用了无参构造方法,再调用addAll()方法,将所有元素添加进去

    4. 常用方法分析

    addFirst(E e)

    源码如下所示:

    public void addFirst(E e) {
            linkFirst(e);
        }
    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++;
        }
    

    addFirst()方法是在链表头部插入一个元素,分析如下:

    • 先获取到原来的头节点,赋值给f
    • 创建一个新的节点newNode,该节点的next节点为f
    • newNode赋值给first
    • 如果原来的头节点是null的话,说明此时链表是空的,添加的是第一个元素,则将newNode也赋值给last节点
    • 如果原来的头节点不是null,那么将原来的头节点fpreNode设置为newNode
    • 链表长度加1,链表修改次数加1

    add(E e)

    源码如下:

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

    add()方法默认是在链表的尾部进行添加元素。

    分析:

    • 此处的linkLast方法是不是很眼熟?和前面的linkeFirst基本一致噢
    • 不同之处仅在于,linkFirst是对对头节点进行变更,而linkLast是对尾节点进行变更
    • 此处不再赘述

    get(int index)

    源码如下所示:

        public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
        }
    

    get()方法内隐藏着LinkedList不便于进行遍历的真相!一定要搞明白哦。

    分析:

    • 第一步先确认index是否在正确的范围内,范围为(0~size)
    • 第二步调用node方法返回对应索引位置的节点元素

    node()方法源码如下:

        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和链表长度的1/2的大小
    • 如果index小于链表长度的1/2,那么就会从头节点向index位置进行遍历,直到获取到相应节点并返回该节点
    • 如果index大于链表长度的1/2,那么从尾节点向index位置进行遍历,直到获取到相应节点并返回该节点

    可以看出,当你访问的元素越靠近链表的中间,那么获取该元素所花费的时间就会越长,所以LinkedList在遍历上是比较慢的,链表本身是不支持任意性访问的,虽然LinkedList的get()方法可以读到相应元素,但是效率很低,不建议使用。

    remove(Object o)

    源码如下所示:

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

    分析如下:

    • 元素为null时,使用==进行元素内容的判断,然后调用unlink方法
    • 元素不为null时,使用equals方法进行判断两个元素是否相同,然后调用unlink方法

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

    哇,unlink方法源代码有点长啊,容我慢慢道来:

    • 定义三个变量分别接收传入节点x的内容、上一个节点、下一个节点
    • 如果节点x的上一个节点为null的话,说明x节点是头节点,那么就将x节点的下一个节点赋值给头节点
    • 如果节点x不是头节点,则将x节点的下一个节点赋值给上一个节点的next节点,并将x节点的上一个节点置为null
    • 经上面两步,已经完成了x节点和上一个节点的断开,以及下一个节点和x节点的上一个节点的链接
    • 如果x节点的下一个节点为null,说明x节点是尾节点,那么就将x节点的上一个节点赋值给last节点
    • 如果x节点不是尾节点,那么将x节点的上一个节点,赋值给下一个节点的prev节点,并将x节点的下一个节点置为null
    • 经过上面几步之后,x节点就已经从链表中移除了
    • 然后将x节点的节点内容置为null,链表长度减1,修改长度记录加1
    • 返回删除节点的内容element

    removeFirst()

    源码如下所示:

        public E removeFirst() {
            final Node<E> f = first;
            if (f == null)
                throw new NoSuchElementException();
            return unlinkFirst(f);
        }
    

    分析:

    • 获取链表头节点,赋值给f
    • 如果f等于null,说明此时链表是空的,抛出异常
    • 如果f不等于null,调用unlinkFirst方法,传入f

    unlinkFirst()方法源码如下所示:

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

    分析:

    • 获取f节点内容,赋值给element变量,获取fnext节点赋值给变量next
    • f节点内容,f节点的next节点,均赋值为null,等待GC回收
    • next节点赋值给first
    • 如果nextnull的话,说明此时链表为空了,所以将尾节点last也赋值为null
    • 否则,将next节点的prev成员变量赋值为null
    • 链表长度减1,修改记录数加1
    • 返回被移除的元素

    5. 其他方法概述

    LinkedList可以作为FIFO(First In First Out)的队列,也就是先进先出的队列使用,以下是关于队列的操作。

        //获取队列的第一个元素,如果为null会返回null
        public E peek() {
            final Node<E> f = first;
            return (f == null) ? null : f.item;
        }
    	//获取队列的第一个元素,如果为null会抛出异常
        public E element() {
            return getFirst();
        }
    	//获取队列的第一个元素,如果为null会返回null
        public E poll() {
            final Node<E> f = first;
            return (f == null) ? null : unlinkFirst(f);
        }
    	//获取队列的第一个元素,如果为null会抛出异常.
        public E remove() {
            return removeFirst();
        }
    	//将元素添加到队列尾部
        public boolean offer(E e) {
            return add(e);
        }
    

    LinkedList也可以作为栈使用,栈的特性是LIFO(Last In First Out),也就是后进先出。 添加和删除元素都只操作队列的首节点即可。

    源码如下:

    	public boolean offerFirst(E e) {
            addFirst(e);
            return true;
        }
    	
        public boolean offerLast(E e) {
            addLast(e);
            return true;
        }
     	
        public E peekFirst() {
            final Node<E> f = first;
            return (f == null) ? null : f.item;
         }
    	
        public E peekLast() {
            final Node<E> l = last;
            return (l == null) ? null : l.item;
        }
    	
        public E pollFirst() {
            final Node<E> f = first;
            return (f == null) ? null : unlinkFirst(f);
        }
    	
        public E pollLast() {
            final Node<E> l = last;
            return (l == null) ? null : unlinkLast(l);
        }
    	
        public void push(E e) {
            addFirst(e);
        }
    	
        public E pop() {
            return removeFirst();
        }
    	
        public boolean removeFirstOccurrence(Object o) {
            return remove(o);
        }
    
    	
    	public boolean removeLastOccurrence(Object o) {
         	
            if (o == null) {
    			for (Node<E> x = last; x != null; x = x.prev) {
                    if (x.item == null) {
                        //调用unlink方法删除指定节点
                        unlink(x);
                        return true;
                    }
                }
            } else {
                for (Node<E> x = last; x != null; x = x.prev) {
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        }
    

    6. 总结

    LinkedList相对于ArrayList而言,源码并没有很复杂,从源码中我们得知了以下相关信息:

    • LinkedList是基于双向链表实现的,即每一个节点都保存了上一个节点和下一个节点的信息
    • LinkedList根据索引获取元素效率低的原因是因为它需要一个节点一个节点的遍历,获取首节点和尾节点很快
    • LinkedList实现了Deque接口,具有双向队列的性质,可以实现数据结构中的堆栈。
    • ...

    知之为知之,不知为不知,是知也。

  • 相关阅读:
    day09 文件操作
    深信服二面
    test1
    视频测试
    通过独立按键控制LED灯
    第一个LED灯
    为什么我的递归调用次数和书上的不一样?
    函数指针数组
    虚拟内存
    单元测试
  • 原文地址:https://www.cnblogs.com/strive-for-life/p/12932845.html
Copyright © 2020-2023  润新知