LinkedList常用源码分析

转载 https://juejin.im/post/5ca23213e51d45439732f84f

一:LinkedList简介

LinkedList是一种链表类型的数据结构,支持高效的插入和删除操作。其实现了Deque接口,使得LinkedList具有队列的特性。LinkedList类的底层实现的数据结构是一个双端的链表。

二:数据结构图分析

链表
如图可以看出LinkedList数据结构使用双向链表结构,有一个头节点和一个尾节点,第一个节点的前驱节点为null,最后一个节点的后继节点为null。其中每个节点有两个指针指向前驱节点和后继节点,这意味着我们在添加和修改LinkedList时不必像ArrayList一样进行扩容操作。

三:继承关系分析

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • 继承于 AbstractSequentialList。而AbstractSequentialList这个类提供了一个基本的 List 接口实现,为实现序列访问的数据储存结构的提供了所需要的最小化的接口实现。他采用的是在迭代器的基础上实现的 get、set、add 和 remove 方法。
  • 实现List接口,说明可以对他进行队列的操作
  • 实现Deque接口,可以将 LinkedList 当作双端队列使用
  • 实现Cloneable接口,说明可以进行克隆
  • 实现Serializable接口,说明可以被序列化

四:源码分析

1:成员变量

// transient修饰的变量在序列化时不会被序列化

//size表示链表中实际元素的数量
transient int size = 0;

//first表示指向第一个节点的指针(头节点)
transient Node(E) first;

//last表示指向最后一个节点的指针(尾节点)
transient Node<E> last;
  • 当链表为空时,first和last一定都为空。
  • 当链表不为空时,first的前驱节点节点一定为空,first.item一定不为空。last的后续节点一定为空,last.item一定不为空。

2:内部类

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; //赋值前驱节点
    }
}

3:构造方法

//构建一个空列表
public LinkedList() {
}

/**
 * 构建一个包含集合c的列表
 *  * @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);
}

4:常用方法

  • 添加元素到第一个节点(头节点)
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f); //调用Node创建一个新的节点
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

分析,通过变量f储存第一个节点,接下来调用Node创建一个新的节点,将新节点作为新first,这是判断,如果之前的first为空,那说明插入之前该链表是空的,那么新插入的节点不仅是first(头)节点而且还是last(尾)节点,所以last要指向新插入的newNode。
如果之前的first(头节点)不是空,那么不动last(尾节点),将first的前驱节点设为新节点,此时原first节点为链表的第二个节点。

1,2,3
1 的前驱节点是null
1 的后继节点是2
2 的前驱节点是1
2 的后继节点是3
3 的前驱节点是2
3 的后继节点是null

  • 添加元素到最后一个节点(尾节点)
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++;
    }

如果之前的last不是空,那么不动first,将last的后继节点设为新节点,此时原last节点为链表的倒数第二个节点,新的节点为尾节点
说明:Fail-Fast 机制
变量modCount为记录当前链表被修改的次数。我们知道LinkedList是线程不安全的,在迭代器遍历链表时,迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断modCount跟expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了。那么将抛出 ConcurrentModificationException。所以当大家遍历那些非线程安全的数据结构时,尽量使用迭代器进行遍历。

重点:向链表中添加元素

public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
	/**
     *在非空节点succ之前插入元素e。
     *List<String>list = new LinkedList<>();
      * list.add("111");
      * list.add("222");
      * list.add("333");
      * list.add("444");
      * list.add("555");
      * list.add("666");
      * list.add(2,"222和333之间");
     */
    void linkBefore(E e, Node<E> succ) { //index为2时,succ是333
        // assert succ != null;
        //保存该节点的前驱节点,这里我么将链表断开,准备插入指定位置  succ保存index后的链表
        final Node<E> pred = succ.prev;//pred==222
        final Node<E> newNode = new Node<>(pred, e, succ);//创建新节点【"222和333之间"】,新节点的前驱节点是pred=222
        //succ的前驱节点(222)指向新节点【将333的前驱节点222替换成"222和333之间"】
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
        //pred的后继节点(333)指向新节点【将222的后继节点333替换成“”222和333之间“”】
            pred.next = newNode;
        size++;//链表长度增加
        modCount++;//操作次数增加
    }
  • list.add(2,“222和333之间”)获取的节点是333用succ表示
  • 然后找到333的前驱节点pred = succ.prev(222)
  • 再找到222的后继节点pred.next(333)
  • 创建新的节点Node newNode = new Node<>(pred,e,succ)[222,222和333之间,333]
  • 把这个新的节点赋值给333的前驱节点和222的后继节点(原来222的后继节点是333,现在把333替换【222和333之间】)
  • 这样的话要插入的数据就在222和333之间了

向链表中添加元素集合

/**
*List<String>list = new LinkedList<>();
        list.add("111");
        list.add("222");
        list.add("333");
        list.add("444");
        list.add("555");
        list.add("666");
        List<String>listlist = new LinkedList<>();
        listlist.add("222和333之间");
        list.addAll(2,listlist);
*
/
public boolean addAll(int index, Collection<? extends E> c) {
		//判断需要插入的位置是否合法 index>0且index小于等于当前链表节点数量
        checkPositionIndex(index);
		//将集合转换为数组
        Object[] a = c.toArray();
        //保存集合长度
        int numNew = a.length;
        if (numNew == 0)
            return false;
		/**
		*创建节点
		*pred前驱节点
		*succ 后继节点
		/ 
        Node<E> pred, succ;
        //如果插入的节点为链表的末端,则前驱节点为原来的尾节点(现在的倒数第二个),后继节点为null
        if (index == size) {
            succ = null;
            pred = last;
        } else {
        //根据节点位置获取该节点
            succ = node(index);
         // 保存该节点的前驱节点,这里我们将链表断开,准备将集合插入指定位置,succ保存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;
                //将前驱改成当前节点,以便后续添加c中其他元素
            pred = newNode;
        }
		// 还是两种判断 如果succ为null,说明插入的节点位置是该链表的尾节点的后面
		//这时通过上面的遍历我们可以知道,pred肯定指向当前链表最后一个节点,所以将last指向pred
        if (succ == null) {
            last = pred;
        } else {
        //此时我们需要将之前断开的链表的后半部分拼接上。pred为当前重组的尾节点
        //则尾节点的后续节点指向succ,succ的前驱节点指向pred
            pred.next = succ;
            succ.prev = pred;
        }
		//链表长度增加
        size += numNew;
        //操作次数增加
        modCount++;
        return true;
    }

获取指定位置的节点

//这里我们可以看到对此查找,LinkedList是做了优化的,
//并没有盲目的去全部遍历,而是判断要查找的坐标离头节点近还是尾节点近,来判断是从头遍历还是从尾开始遍历
Node<E> node(int index) {
    // assert isElementIndex(index);
	//size>>1就是size/2的意思,取出中间位置和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;
    }
}

删除一个元素(元素不为空)

/**
*List<String>list = new LinkedList<>();
        list.add("111");
        list.add("222");
        list.add("333");
        list.add("444");
        list.add("555");
        list.add("666");
        list.remove(2);
*
*
/
E unlink(Node<E> x) {
    // assert x != null;
    //当前节点 最后要把这个节点返回去
    final E element = x.item; //333
    //存储待删除节点的后续节点
    final Node<E> next = x.next; //444
    //存储待删除节点的前驱节点
    final Node<E> prev = x.prev; //222
    //如果待删除节点的前一个节点为空,表明待删除的节点为头结点。
    //需要把待删除节点的后一个节点设置为头结点。
    if (prev == null) {
        first = next;
    } else {
        //把待删除的节点的前、后节点链接起来
        prev.next = next;//【prev.next(333)==next(444)】
        x.prev = null;// 待删除的前置节点至为null
    }

    //如果待删除节点是尾节点
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;//【next.prev(333)==prev(222)】
        x.next = null;
    }

    x.item = null;
    //元素长度减一
    size--;
    //操作次数加一
    modCount++;
    return element;
}

总结

元素源码我们可以看出LinkedList在添加删除元素时,不需要像ArrayList一样去扩容。而是根据元素所在位置去查找元素时又不像ArrayList中一样直接去取,而是需要遍历。所以LinkedList更多的适用于频繁添加删除,而查找不多的场景下。
根据位置查找元素时先判断离头节点近还是尾节点近这种优化方式我们可以用于日常的代码中
LinkedList是线程不安全的,在遍历的过程中我们尽量使用迭代器进行遍历

猜你喜欢

转载自blog.csdn.net/qq_32963927/article/details/89353128