美文网首页
LinkedList源码分析

LinkedList源码分析

作者: 知道越多不知道越多 | 来源:发表于2021-04-22 17:03 被阅读0次

    前言

    LinkedList与Vector和ArrayList存在很大的不同,底层使用链表实现,也正是这个数据结构注定它的使用场景和其它两个不同。我们都知道数组读取的速度是O(1),但删除和写入数据可能涉及到数组的移动,所以效率会慢一些,而链表和数组正好相反,链表的读写速度慢,到操作速度快,只需要改变指针指向即可,一起看看它是如何实现的。

    LinkedList

    LinkedList是List接口的常用实现,对于一些读少写多的场景是非常合适的,它实现了Deque接口,所以拥有双向链表的功能,LinkedList是无法指定容量大小的,所以不存在扩容的说法,它会根据数据的大小,自动伸缩,但是如果链表无限长的话,查询效率是极低的,也可能会发生OOM。

    源码分析

    一下源码是基于JDK 1.8来分析,如果发现和你看的源码不一样,那得看看你的源码的版本哦。

    重要的属性

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

    重要的属性就是上边的三个,其实很容易看出来,实现双向链表是基于first和last来实现的。

    构造方法

    // 无参构造方法
    public LinkedList() {
    }
    // 初始化容器的时候把传入的数据存到容器里 
    public LinkedList(Collection<? extends E> c) {
      this();
      // 后边说
      addAll(c);
    }
    

    构造函数很简单,因为是使用链表存储,所以不需要指定容器大小。

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

    存储数据的方法

    容器提供了三总数据存储的方法,我们挨个分析
    add(E e)
    边看源码边分析

    public boolean add(E e) {
      // 看方法名应该能猜出来,是将数据写到末尾
      linkLast(e);
      return true;
    }
    
    void linkLast(E e) {
       // 记录一下last尾节点
       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++;   // 容器数据+1
       modCount++;  // modCount 的作用不分析,看前Vecotor的分析  
    }
    

    当我们通过add(E e)方法将数据写入容器时,会将新的数据写入链表的尾部

    add(int index,E e)
    指定插入的位置

    public void add(int index, E element) {
      // 验证index,其实就是和size进行比较
      checkPositionIndex(index);
      if (index == size) // 说明插入末尾,和add(E e)是一样的
          linkLast(element);
      else
          linkBefore(element, node(index)); // 在中间的位置插入
    }
    /*
     *  这个方法的主要目的是找出要插入位置的节点
     * 因为是双向链表,所以可以从头尾开始查询
     */
    Node<E> node(int index) {
            // assert isElementIndex(index);
         // size >> 1 其实就是获取size的中点
         if (index < (size >> 1)) { // 说明节点在前半部分,从头结点开始查
             Node<E> x = first;
            // 只能for循环挨个比较
            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;
        }
    }
    /*
     *  插入数据的方法:找到要插入的位置节点就很简单了,只需要移动指针就可以
     *  @param e 要插入的值
     *  @param succ 要插入位置的节点
     */
    void linkBefore(E e, Node<E> succ) {
       // assert succ != null;
       // 记录插入位置的上一个节点
       final Node<E> pred = succ.prev;
       // 创建新节点,并将它的下一个节点指向succ
       final Node<E> newNode = new Node<>(pred, e, succ);
       // 将succ 的上一个节点指向新节点
       succ.prev = newNode;
       if (pred == null)  // 说明succ是头结点
            first = newNode;  // 将first指向新的节点即可
       else
           pred.next = newNode; // 将pred的下一个节点指向新的节点
       size++;   // 容器数据+1
       modCount++;
    }
    

    主要步骤如下:

    1. 验证index的正确性: index >= 0 && index <= size
    2. 找到链表中index位置的节点 succ
      2.1 为了加快查询效率,先找到index在链表的前半部分还是后半部分
      2.2 如果是前半部分,从first开始查,反之从last开始查
    3. 先记录index位置节点的前一个节点 pre
    4. 创建新的节点newNode,并将newNode.next = succ
    5. 如果pre为空,说明是头节点,将first = newNode,反之pre.next = newNode
    6. size++ , modCount++

    addFirst
    将新的元素新增到链表的头部

    public void addFirst(E e) {
       // 啥也不干,交给linkFirst处理
       linkFirst(e);
    }
    
    private void linkFirst(E e) {
      // 先记录头结点
      final Node<E> f = first;
      // 创建新节点,并将它的next指向头结点
      final Node<E> newNode = new Node<>(null, e, f);
      first = newNode;
      if (f == null)
       last = newNode;
     else
       f.prev = newNode;
     size++;
      modCount++;
    }
    

    以上就比较简单了,将新的节点放到头结点,只是简单的节点引用指向改变就能实现

    addLast
    将新的节点放入链表尾部,这个方法和add(E e)是一样的,都是交给linkLast方法去做,这里就不再分析了。

    set(int index,E e)
    LinkedList也提供了更新数据的方法,逻辑都一样,先通过node(index)方法找到要修改的节点,然后将值进行替换,返回旧值,这里不分析啦,自己看吧。

    get(int index)
    get方法其实很简单,走的还是node(index) 方法,先找到节点,然后返回节点值,不分析了

    LinkedList拥有了头节点和尾节点两个属性,所以对于操作头结点和尾节点来说,很方便,所以它可以实现栈,也可实现队列
    peekFirst
    返回链表头部的值,不删除头结点

    public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }
    

    pollFirst
    返回头结点,并删除头结点

    public E pollFirst() {
      final Node<E> f = first;
      // 删除头节点是在unlinkFirst方法里做
      return (f == null) ? null : unlinkFirst(f);
    }
    // 删除头节点,并返回旧头节点的值
    // 入参 f就是要删除的节点
    private E unlinkFirst(Node<E> f) {
      // assert f == first && f != null;
     // 先记录头节点的值
      final E element = f.item;
      // 获取要删除节点的下一个节点
      final Node<E> next = f.next;
      // 优秀的代码是为别人考虑的,将引用置空,让GC尽快回收
      f.item = null;
      f.next = null; // help GC
      // 将头节点指向下一个节点
      first = next;
      if (next == null)
            last = null; // 说明只有一个节点
       else
          next.prev = null; // 将next节点的上一个节点指向null,因为它已经是头结点
      size--;   // 容器数据-1
      modCount++;  
      return element;
    }
    

    其实也很简单,我相信peekLast和pollLast方法不需要我分析了吧,知道了操作原理,怎么操作都很容易明白

    remove
    remove方法是用来删除容器数据,默认删除的就是头节点的数据,你一定想到了吧,没错,做事的还是unlinkFirst方法,已经分析过了。

    案例
    基于LinkedList的特性,我们可以用来实现队列 和 栈 ,简单写两个demo
    队列(先进先出)

    public class LinkedListStudy {
    
        public static void main(String[] args) {
            MyQueue myQueue = new MyQueue();
            myQueue.push(1);
            myQueue.push(2);
            myQueue.push(3);
            System.out.println(myQueue.pop());
            System.out.println(myQueue.pop());
        }
    
        private static class MyQueue{
            LinkedList<Integer> list = new LinkedList<>();
    
            public void push(Integer num){
                list.addFirst(num);
            }
    
            public Integer pop(){
                if(list.size() == 0){
                    return null;
                }
                return list.pollLast();
            }
        }
    }
    输出
    1
    2
    3
    

    栈(先进后出)

    public class LinkedListStudy {
    
        public static void main(String[] args) {
            MyStack myStack = new MyStack();
            myStack.push(1);
            myStack.push(2);
            myStack.push(3);
            System.out.println(myStack.pop());
            System.out.println(myStack.pop());
        }
    
        private static class MyStack {
            LinkedList<Integer> list = new LinkedList<>();
    
            public void push(Integer num){
                list.addFirst(num);
            }
    
            public Integer pop(){
                if(list.size() == 0){
                    return null;
                }
                return list.pollFirst();
            }
        }
    }
    输出
    3
    2
    

    总结

    1. LinkedList底层使用链表存储,容器容量不受限制
    2. 因为使用链表存储,所以查询效率低,不过通过双向链表可以稍微缓解一些,但是操作数据的效率很高
    3. 由于使用的是双向链表,所以可以当做队列和栈来使用。

    以上就是我分析,如果问题,烦请支出更正,一起学习。

    我是一个爱看源码的老谢,知道越多,不知道的越多。

    相关文章

      网友评论

          本文标题:LinkedList源码分析

          本文链接:https://www.haomeiwen.com/subject/zpyxrltx.html