LinkedList源码分析(jdk1.8)

一、概述

在这里插入图片描述
上图为LinkedList的继承结构图,根据继承关系总结如下:

LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是非同步的。

二、源码分析

1、成员变量

   //集合元素数量
    transient int size = 0;

   //链表头结点
    transient Node<E> first;

    //链表尾结点
    transient Node<E> last;

LinkedList的属性不多,size表示其元素数量,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;
        }
    }

结点(Node)为LinkedList的静态内部类,且有前置节点和后置节点 ,只是头节点的前置节点为空,而尾节点的后置节点为空,说明LinkedList内部为双向链表结构。可以用如下的图形象的表示:
在这里插入图片描述

2、构造方法

   //默认构造方法
    public LinkedList() {
    }

   //通过集合c的元素构造链表
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);//将集合c中的所有元素添加到LinkedList中
    }

不传参的构造方法创建空的链表,将集合作为入参的构造方法则通过集合c的元素创建链表,调用了addAll方法,addAll方法的原理将在下面讲解。

3、常用方法

我们从LinkedList集合的常用操作入手,比如集合的增删改查,栈的进栈和出栈,队列的进队列和出队列等。

3.1、增

方法名 功能
linkFirst(E e) 添加元素e到链表的头部
linkLast(E e) 添加元素e到链表的尾部
addFirst(E e) 添加元素e到链表的头部,调用 linkFirst方法,无返回值
addLast(E e) 添加元素e到链表的尾部,调用linkLast方法,无返回值
add(E e) 添加元素e到链表的尾部,调用linkLast方法
add(int index, E element) 添加元素e到链表的指定位置index处
offer(E e) 添加元素e到链表的尾部,调用 add(E e) 方法
offerFirst(E e) 添加元素e到链表的头部,调用 addFirst(E e)方法,返回true或false
offerLast(E e) 添加元素e到链表的尾部,调用 addLast(E e)方法,返回true或false
addAll(Collection<? extends E> c) 添加集合c的所有元素到链表的尾部
addAll(int index, Collection<? extends E> c) 添加集合c的所有元素到链表的指定位置 处
linkBefore(E e, Node succ) 将元素e插入到链表的指定节点succ前
push(E e)(E e, Node succ) 将元素e插入到栈上,无返回值,其实就是调用了addFirst(E e)方法

有上面的表格可知,只要理解了linkFirst,linkLast方法的源码,大部分方法的原理都搞懂了,首先来看下linkFirst,linkLast的源码:

   //链表的头部插入元素e
    private void linkFirst(E e) {
        //获取链表的头节点
        final Node<E> f = first;
        //通过元素e创建新节点,其前置节点为空,后置结点为当前链表的头节点
        final Node<E> newNode = new Node<>(null, e, f);
        //将链表的头结点设置为新节点
        first = newNode;
        //若原本的头节点为空,说明为空链表,将尾结点也设置为新节点;若原本的头节点不为空,就将其前置结点设置为新节点;
        //此时完成了头部插入节点的操作
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
         //将元素大小和链表修改记录分别+1
        size++;
        modCount++;
    }
  
  //链表的尾部插入元素e
    void linkLast(E e) {
        //获取链表的头结点
        final Node<E> l = last;
         //通过元素e创建新节点,其前置结点当前列表的尾节点,后置节点为空
        final Node<E> newNode = new Node<>(l, e, null);
        //将链表的尾结点设置为新节点
        last = newNode;
         //若原本的尾节点为空,说明为空链表,将头结点也设置为新节点;若原本的尾节点不为空,就将其后置结点设置为新节点;
        //此时完成了尾部插入结点的操作
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
         //将元素大小和链表修改记录分别+1
        size++;
        modCount++;
      }

    public void addFirst(E e) {
        linkFirst(e);
    }
    public void addLast(E e) {
    	linkLast(e);
	   }
       public E offer(E e) {
   	      return add(e);
      }
      public boolean offerFirst(E e) {
   		 addFirst(e);
   		 return true;
     }
    public boolean offerLast(E e) {
    	addLast(e);
   		 return true;
    }
  public void push(E e) {
   	      addFirst(e);
 }

add(int index, E element) :在指定位置添加元素
在add(int index, E element)方法中调用了node(int index) 来获取指定下标的节点,很多地方也调用了该方法。它采取了二分法的方式去获取值,可以提高执行效率。

        //返回指定位置的节点
	 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;
	        }
	    }

了解了node(int index)后再进一步看add(int index, E element) 方法的执行过程

    public void add(int index, E element) {
        //检测下标的边界是否合法
        checkPositionIndex(index);

        if (index == size)
          //若index == size说明往尾部插入元素,直接调用linkLast方法;否则调用linkBefore方法
            linkLast(element);
        else
            linkBefore(element, node(index));
    }	       
       //将元素e插入到链表的指定节点succ前
	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++;
	    }

add(int index, E element)的执行过程可以用下图表示,可以知道LinkedList的随机插入效率比ArrayList高,因为ArrayList需要移动数组中相应的元素而它不需要。
在这里插入图片描述

addAll方法
addAll(Collection<? extends E> c):在尾部插入集合c中的元素
addAll(int index, Collection<? extends E> c):在指定位置index插入集合c中的元素

//在尾部插入集合c中的元素
 public boolean addAll(Collection<? extends E> c) {
       //调用addAll(int index, Collection<? extends E> c)方法,当index=size时就是从尾部位置插入
        return addAll(size, c);
   }
 
 //在指定位置index插入集合c中的元素
 public boolean addAll(int index, Collection<? extends E> c) {
 	    //检测插入位置的边界
        checkPositionIndex(index);
	
	    //将集合转换成数组
        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;
	
	//index节点的前置节点,后置节点
        Node<E> pred, succ;
        //当index=size时就是从尾部位置插入,此时后置节点设为空,前置节点设为当前链表的尾节点
        //否则表示从指定位置插入,此时后置节点设为指定下标的节点,前置节点设为指定下标节点的前置节点
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            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) {//循环结束后,判断,如果后置节点是null。 说明此时是在队尾append的。
            last = pred;//将尾节点设置为后置节点
        } else {
            pred.next = succ;//否则是在队中插入的节点 ,更新前置节点的后置节点
            succ.prev = pred;//再更新后置节点的前置节点
        }

        size += numNew;
        modCount++;
        return true;
    }		

addAll方法主要分为4步:

  1. 检查index索引的边界值
  2. 将集合数据 转换为数组
  3. 得到插入位置index的前置节点和后置节点
  4. 遍历数据,将数据插入到指定位置

3.2、删

方法名 功能
unlink(Node x) 删除节点
removeFirst 删除首节点并返回该删除首节点,调用方法 unlinkFirst(Node f)
removeLast 删除尾节点并返回该删除尾节点,调用方法 unlinkLast(Node l)
remove(int index) 删除指定位置节点并返回该删除节点
remove(Object o) 删除首次出现的指定的节点,删除成功返回true,不存在返回false
removeFirstOccurrence(Object o) 删除首次出现的指定的节点,删除成功返回true,不存在返回false,调用方法remove(Object o)
removeLastOccurrence(Object o) 删除最后一次出现的指定的节点,删除成功返回true,不存在返回false
poll 检索并删除此链表的头节点,不存在返回空
pollFirst 检索并删除此链表的头节点,不存在返回空
pollLast 检索并删除此链表的尾节点,不存在返回空

unlink(Node x):删除指定节点x

 //取消链表的非空节点x
  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;
    }

removeFirst:删除首节点并返回该删除首节点,调用方法 unlinkFirst(Node f)

public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
   
 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; //首节点置为空,剩下的交给GC处理
        first = next;//将链表的首节点设为后置节点
        if (next == null)
            last = null;//若后置节点为空,说明为尾节点,此时将尾节点设为空
        else
            next.prev = null;//若后置节点不为空,将其前置节点设为空,此时相当于变成了首节点
        size--;
        modCount++;
        return element;
    }	 

removeLast :删除尾节点并返回该删除尾节点,调用方法 unlinkLast(Node l)

    public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }
     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; //尾节点置为空,剩下的交给GC处理
        last = prev;//将链表的尾节点设为前置节点
        if (prev == null)
            first = null;//若前置节点为空,说明为首节点,此时将首节点设为空
        else
            prev.next = null;//若前置节点不为空,将其后置节点设为空,此时相当于变成了尾节点
        size--;
        modCount++;
        return element;
    }

remove(int index): 删除指定位置节点并返回该删除节点

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

删除流程主要为:
1.得到待删除节点的前置节点和后置节点
2.删除前置节点
3.删除后置节点
在这里插入图片描述

remove(Object o):删除首次出现的指定的节点,删除成功返回true,不存在返回false。
该方法就是遍历元素然后匹配的话就调用删除节点方法unlink(Node x)。

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

3.3、改

set(int index, E element):设置指定位置的元素并返回原来的元素

	public E set(int index, E element) {
	//检查索引边界值
        checkElementIndex(index);
        //获取当前位置的节点
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

3.4、查

方法名 功能
node(int index) 获取指定位置处的节点
get(int index) 获取指定位置处的元素
getFirst 获取链表的第一个元素
getLast 获取链表的最后一个元素
element 获取链表的第一个元素,调用方法getFirst
peek 获取链表的第一个元素 ,不存在则返回空
peekFirst 获取链表的第一个元素 ,不存在则返回空
peekLast 获取链表的最后一个元素 ,不存在则返回空

node(int index)方法在前面已经提过,它通过二分法的方式缩小遍历范围,一定程度上提高了检索效率,不过肯定比不上ArrayList的检索效率。

get(int index):获取指定位置处的节点

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

getFirst:获取链表的第一个元素

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

getLast:获取链表的最后一个元素

    public E getLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return l.item;
    }

三、总结

  • LinkedList不是线程安全的。
  • LinkedList底层基于双向链表,随机插入和删除效率比ArrayList高,但跟据索引查询的效率比ArrayList低。
  • LinkedList相比ArrayList没有扩容操作,ArrayList可能会浪费一定的容量空间,但LinkedList每个节点都要存储前后节点的引用,需要消耗相当的空间,就存储密度来说,ArrayList优于LinkedList。
  • 可以使用LinkedList作为队列和栈的实现。

参考链接:
https://www.imooc.com/article/78970
https://blog.csdn.net/zxt0601/article/details/77341098
https://blog.csdn.net/qq_19431333/article/details/54572876
https://www.cnblogs.com/xujian2014/p/4630785.html#_label2

猜你喜欢

转载自blog.csdn.net/sunjian1122/article/details/84621729