LinkedList源码解读

1、LinkedList简介

  通过对ArrayList的源码分析知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点。本文介绍的LikedList是List接口的另一种实现,它的底层是基于双向链表实现的,因此它具有插入删除快而查找修改慢的特点,此外,通过对双向链表的操作还可以实现队列和栈的功能。下面我们就来看看LinkedList是怎么实现的。

下面是LinkedList的UML图:

  1. 继承了AbstractSequentialList抽象类:在遍历LinkedList的时候,官方更推荐使用顺序访问,也就是使用我们的迭代器。(因为LinkedList底层是通过一个链表来实现的)(虽然LinkedList也提供了get(int index)方法,但是底层的实现是:每次调用get(int index)方法的时候,都需要从链表的头部或者尾部进行遍历,每一的遍历时间复杂度是O(index),而相对比ArrayList的底层实现,每次遍历的时间复杂度都是O(1)。所以不推荐通过get(int index)遍历LinkedList。至于上面的说从链表的头部后尾部进行遍历:官方源码对遍历进行了优化:通过判断索引index更靠近链表的头部还是尾部来选择遍历的方向)(所以这里遍历LinkedList推荐使用迭代器)。
  2. 实现了List接口。(提供List接口中所有方法的实现)
  3. 实现了Cloneable接口,它支持克隆(浅克隆),底层实现:LinkedList节点并没有被克隆,只是通过Object的clone()方法得到的Object对象强制转化为了LinkedList,然后把它内部的实例域都置空,然后把被拷贝的LinkedList节点中的每一个值都拷贝到clone中。(详见后文源码解析)
  4. 实现了Deque接口。实现了Deque所有的可选的操作。
  5. 实现了Serializable接口。表明它支持序列化。(和ArrayList一样,底层都提供了两个方法:readObject(ObjectInputStream o)、writeObject(ObjectOutputStream o),用于实现序列化,底层只序列化节点的个数和节点的值)。

1.1、数据结构

首先我们先看看LinkedList的核心,也就是LinkedList中真正用来存储元素的数据结构。

Node 类是LinkedList中的私有内部类,LinkedList中就是通过Node来存储集合中的元素。

E :节点的值。

Node next:当前节点的后一个节点的引用(可以理解为指向当前节点的后一个节点的指针)

Node prev:当前节点的前一个节点的引用(可以理解为指向当前节点的前一个节点的指针)

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组成 双向链表 的结构

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    //元素个数
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
     //指向第一个节点
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
     指向最后一个节点
    transient Node<E> last;
    
    }

 

size:用来记录LinkedList的大小

Node first:用来表示LinkedList的头节点

Node last:用来表示LinkedList的尾节点

2、关键方法

2.1、构造函数

有两个构造函数,一个是初始化一个空的实例,另外一个是传入一个集合进行初初始化。

/**
 * Constructs an empty list.
 空的构造函数
 */
public LinkedList() {
}

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 * 传入一个集合(Collection)作为参数初始化LinkedList。
 * @param  c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public LinkedList(Collection<? extends E> c) {
    this();//首先调用一下空的构造器
    addAll(c); //这里调用了addAll(),就是插入所有元素
}

 

在初始化的时候主要调用了addAll()方法,那么这个addAll()方法是怎么样添加元素的呢。

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

//插入给定集合的元素,从指定的index开始插入
//index 插入节点的位置
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index); //检查索引位置是否合法,必须得 >=0 && <=size 否则抛出IndexOutOfBoundsException异常

    Object[] a = c.toArray();//将集合转换为数组,why?
    int numNew = a.length; //集合(数组)的长度
    if (numNew == 0) 
        return false; //如果待添加的集合为空,直接返回,无需进行后面的步骤。后面都是用来把集合中的元素添加到LinkedList中。

    Node<E> pred, succ; //pred:指待添加节点的前一个节点;succ:指待添加节点的位置
    //这里分为两个分支
    if (index == size) { //当index=size时说明 新添加LinkedList中的集合中的每一个元素都是在LinkedList最后面
        succ = null; //标记 succ 为null
        pred = last; //待添加节点的前一个节点为尾结点last
    } else { //否则非末尾添加
        succ = node(index); //查找待添加位置的节点,node(index)方法详解见后文
        pred = succ.prev;  // pred指向succ节点的前一个节点
    }

    for (Object o : a) {
        //接着遍历数组中的每个元素。在每次遍历的时候,都新建一个节点,该节点的值存储数组a中遍历的值,该节点的prev用来存储pred节点,next设置为空。
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
        //如果该节点的前一个节点为空,则把当前节点设置为头节点
            first = newNode;
        else
        //否则的话就把当前节点的前一个节点的next值设置为当前节点
            pred.next = newNode;
        //最后把pred指向当前节点,以便后续新节点的添加。    
        pred = newNode;
    }
//这里仍然和上面一样,分两种情况对待:
    if (succ == null) {
        //当succ==null,新添加的节点位于LinkedList集合的最后一个元素的后面
        last = pred; //通过遍历上面的a的所有元素,此时pred指向的是LinkedList中的最后一个元素,所以把last指向pred指向的节点
    } else {//当不为空的时候,表明在LinkedList集合中添加的元素,需要把pred的next指向succ上,succ的prev指向pred
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;//最后把集合的大小设置为新的大小
    modCount++;//modCount(修改的次数)自增
    return true;
}

下面对上面两种添加集合的方式通过图示说明:

 

 

 

Object[] a = c.toArray();//将集合转换为数组,why?
通过该方法的注释可知:这个数组是不会被其他线程所访问到,因此 是 “安全” 的

/**
 * Returns an array containing all of the elements in this list
 * in proper sequence (from first to last element).
 * 返回一个数组,该数组按正确顺序(从第一个元素到最后一个元素)包含此列表中的所有元素。
 
 * <p>The returned array will be "safe" in that no references to it are
 * maintained by this list.  (In other words, this method must allocate
 * a new array).  The caller is thus free to modify the returned array.
 * 返回的数组将是“安全的”,因为此列表不维护对它的引用。(换句话说,这个方法必须分配一个新的数组)。因此,调用者可以自由修改返回的数组
 
 * <p>This method acts as bridge between array-based and collection-based
 * APIs.
 这个方法是集合与方法之间的桥梁APIS
 * 
 * @return an array containing all of the elements in this list
 *         in proper sequence
 */
public Object[] toArray() {
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;
}

/**
 * Returns the (non-null) Node at the specified element index.
 */
Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) { //先比较一下index更靠近链表(LinkedList)的头节点还是尾节点,然后进行遍历,获取相应的节点。
        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;
    }
}

 

2.2、一些辅助方法

这些方法大多数是private或者是protected修饰的,LinkedList中大多数通过他们来完成List、Deque中类似的操作。

2.2.1、void linkLast(E e) 把参数中的元素作为链表的最后一个元素。

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last; //标记尾结点为l
    final Node<E> newNode = new Node<>(l, e, null);//新建节点的prev为l
    last = newNode;//将新建节点置为last尾结点
    if (l == null) //如果l为null,说明原来的LinkedList为空,那么新建节点既是尾结点也是头结点
        first = newNode;
    else//否则将l节点的后指针next指向新节点
        l.next = newNode;
    size++; //size和modCount自增。
    modCount++;
}

2.2.2、private void linkFirst(E e) 把参数中的元素作为链表的第一个元素

/**
 * Links e as first element.
 */
private void linkFirst(E e) {
    final Node<E> f = first; //标记头结点为f
    final Node<E> newNode = new Node<>(null, e, f);//新建节点的next为f
    first = newNode;//将新建节点置为first头节点
    if (f == null)//如果头节点为null,说明原来的LinkedList为空,那么新节点既是头节点也是尾结点
        last = newNode;
    else//否则就将f节点的前指针prev指向新节点
        f.prev = newNode;
    size++;//size和modCount自增
    modCount++;
}

2.2.3、void linkBefore(E e, Node<E> succ) 在非空节点succ之前插入元素e

/**
 * Inserts element e before non-null Node succ.
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev; //首先用pred标间插入节点位置的前一个节点
    final Node<E> newNode = new Node<>(pred, e, succ); //之后插入节点e的前置节点就是pred,后置节点是succ
    succ.prev = newNode;//插入后succ的前置位置就是新节点newNode
    if (pred == null)//如果pred为null那么newNode就是头结点
        first = newNode;
    else//否则给pred节点设置后一个节点是newNode
        pred.next = newNode;
    size++;
    modCount++;
}

2.2.4、private E unlinkFirst(Node<E> f) 删除LinkedList中第一个节点(该节点不为空)(并且返回删除的节点的值)

官方文档的代码中也给出了注释:使用该方法的前提是参数f是头节点,而且f不能为空。不过,它是私有方法,我们也没有权限使用

/**
 * Unlinks non-null first node f.
 */
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item; //用element来标记头节点的值
    final Node<E> next = f.next;//用next节点来标记头节点的下一个节点
    f.item = null; //将头节点的item置为null
    f.next = null; // help GC//将头节点的下一个节点指针置为null,这样头节点就没有被任何引用,等待GC回收
    first = next;//将原本头节点的下一个节点设置为新的头节点
    if (next == null)//如果next节点为null,那么这个集合只有一个头节点,删除后,为空,需要把last设置为空
        last = null;
    else//否则将新头节点的prev设为null
        next.prev = null;
    size--;
    modCount++;
    return element;
}

2.2.5、private E unlinkLast(Node<E> l) 删除LinkedList的最后一个节点。(该节点不为空)(并且返回删除节点对应的值)

和unlinkFirst()方法思路差不多

/**
 * Unlinks non-null last node 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; // help GC
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

2.2.6、E unlink(Node<E> x) 删除一个节点(该节点不为空)

/**
 * Unlinks non-null node x.
 */
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item; //用element标记删除节点的值
    final Node<E> next = x.next;//用next标记删除节点的后置节点
    final Node<E> prev = x.prev;//用prev标记删除节点的前置节点

    if (prev == null) {//如果prev为null那么删除的是前置节点
        first = next;//将next设置为新的头节点
    } else {
        prev.next = next;//否则prev的后置节点为next
        x.prev = null;//删除节点的前置指针设为null,等待垃圾回收
    }

    if (next == null) {//如果next为null,那么删除的是尾结点
        last = prev;//将prev设置为尾结点
    } else {
        next.prev = prev;//否则next的后置节点设为prev
        x.next = null;//删除节点的后置节点指针设为null,等待垃圾回收
    }

    x.item = null;//最后将删除节点的值设为null,等待垃圾回收
    size--;
    modCount++;
    return element;
}

到目前大部分的私有方法全部讲完了,而提供给用户的方法大多直接调用了上述方法

2.2.7、提供给用户的直接方法

①提供给用户使用的删除头结点,并返回删除的值。直接调用了上面的工具方法unlinkFirst(Node f)

/**
 * Removes and returns the first element from this list.
 *
 * @return the first element from this list
 * @throws NoSuchElementException if this list is empty
 */
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

②删除链表中的最后一个节点,并返回被删除节点的值。和上面一样调用了unlinkLast(Node last)方法。

/**
 * Removes and returns the last element from this list.
 *
 * @return the last element from this list
 * @throws NoSuchElementException if this list is empty
 */
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

③ 在LinkedList头部添加一个新的元素、尾部添加一个新元素。都是调用了私有方法。

/**
 * Inserts the specified element at the beginning of this list.
 *
 * @param e the element to add
 */
public void addFirst(E e) {
    linkFirst(e);
}
/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #add}.
 *
 * @param e the element to add
 */
public void addLast(E e) {
    linkLast(e);
}

④添加一个新元素。直接在最后面添加,调用了linkLast()方法。

/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #addLast}.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

还有很多简单方法,不再一一举例

3、总结

不管是单向队列还是双向队列还是栈,其实都是对链表的头结点和尾结点进行操作,它们的操作和linkBefore()和unlink()类似,只不过一个是对链表两端操作,一个是对链表中间操作。可以说这四个方法都是linkBefore()和unlink()方法的特殊情况,因此不难理解它们的内部实现,在此不多做介绍。到这里,我们对LinkedList的分析也即将结束,对全文中的重点做个总结:

  1. LinkedList是基于双向链表实现的,不论是增删改查方法还是队列和栈的实现,都可通过操作结点实现
  2. LinkedList无需提前指定容量,因为基于链表操作,集合的容量随着元素的加入自动增加
  3. LinkedList删除元素后集合占用的内存自动缩小,无需像ArrayList一样调用trimToSize()方法
  4. LinkedList的所有方法没有进行同步,因此它也不是线程安全的,应该避免在多线程环境下使用
  5. 以上分析基于JDK1.8,其他版本会有些出入,因此不能一概而论

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值