万字长文之JDK1.8的LinkedList源码解析

本文详细介绍了Java LinkedList的源码实现,包括其作为List和Deque接口的实现,以及核心字段和操作方法如add、remove、get等。通过源码分析展示了LinkedList的双向链表结构,探讨了迭代器与传统for循环在效率上的差异,并通过实例验证了LinkedList的clone方法是浅拷贝。
摘要由CSDN通过智能技术生成

1.前言

上一篇文章我们看了List集合的数组实现JDK1.8的ArrayList 源码解析,走过路过不要错过,本篇文章我们将介绍 List 集合的链表实现 LinkedList。

2.整体架构

和 ArrayList 一样,LinkedList 集合也实现了Cloneable接口,可以支持克隆。

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

各接口,类的关系如下: 

2.1 cloneable接口

我们可以看下LinkedList中如何重写clone方法的,具体代码如下,我们可以看到其只是浅拷贝,真正的数据并没有拷贝。

// 返回此其浅表副本(元素本身不会被克隆。)
public Object clone() {
    LinkedList<E> clone = superClone();

    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;

    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);

    return clone;
}

口说无凭,我们来看下demo, 是不是浅拷贝。

 public static void main(String[] args){
        LinkedList list=new LinkedList();
        list.add("学习");
        list.add("Java");
        list.add("的");
        list.add("小姐姐");
        System.out.println(list);
        Object otherList=list.clone();
        System.out.println(otherList);
    }

 我们从下面的图可以看出list的各元素地址为547,548,549,552,和otherlist是一样的,但是list的地址为541,otherlist的地址为554,说明他们地址不一样。

这也就验证了上面的说法,linkedlist的clone方法是浅拷贝,返回的只是浅表副本,真正的元素并没有拷贝

 

2.2Deque接口 

相对于 ArrayList 集合,LinkedList 集合多实现了一个 Deque(双向队列接口 接口,其两端都可以进行增加和删除操作。

Deque是一个线性collection,支持在两端插入和移除元素。名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。

此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。插入操作的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现中,插入操作不能失败。

下表总结了上述 12 种方法:

 第一个元素 (头部) 最后一个元素 (尾部)
 抛出异常 特殊值 抛出异常特殊值
插入 addFirst(e)offerFirst(e) addLast(e) offerLast(e)
删除removeFirst() pollFirst() removeLast()pollLast()
检查getFirst()peekFirst() getLast() peekLast()

   
   
如上翻译于jdk1.8的LinkedList注释,如下图:


             

  3.字段属性

主要包括三个核心字段,分别是节点数量size,第一个节点的指针first,最后一个节点的指针last。但是其有个内部类Node,item字段存放具体的数据。

//节点数量
transient int size = 0;

//第一个节点的指针
transient Node<E> first;

//最后一个节点的指针
transient Node<E> last;
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;
      }
}

LinkedList 底层数据结构是一个双向链表,整体结构如下图所示:

 

 

上图代表了一个双向链表结构,链表中的每个节点都可以向前或者向后追溯,我们有几个概念如下:
• 链表每个节点我们叫做 Node,Node 有 prev 属性,代表前一个节点的位置,next 属性,代表后一个节点的位置
• first 是双向链表的头节点,它的前一个节点是 null,所以其prev属性为null;
• last 是双向链表的尾节点,它的后一个节点是 null,所以其next属性为null;
• 当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null;
• 因为是个双向链表,只要机器内存足够强大,是没有大小限制的。


4.源码解析

4.1构造函数

 LinkedList 有两个构造函数,一个是默认的空构造函数,另一个是将具体的集合初始化到LinkedList中。

 注意:LinkedList 是没有初始化链表大小,其没有确定的大小,只有添加一个元素才会增加大小,是通过修改指针的地址来实现元素的增加。

 

4.1.1无参构造

public LinkedList() {
}


4.1.2有参构造

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

4.2 新增

4.2.1 从头部追加addFirst

 

public void addFirst(E e) {
        linkFirst(e);
}

private void linkFirst(E e) {
        //定义f节点,暂存头节点
        final Node<E> f = first;

        //创建新的节点元素,为目标值
        final Node<E> newNode = new Node<>(null, e, f);

        //头节点赋值为新的节点元素
        first = newNode;

        //如果f节点为null,说明原来为空链表,即尾指针也指向新的节点元素
        if (f == null)
            last = newNode;
        
        //如果不是null,说明原来不为空链表,即f节点的头指针为新的
        else
            f.prev = newNode;

        //调整大小和版本
        size++;
        modCount++;
}

4.2.2 从尾部追加addLast

public void addLast(E e) {
        linkLast(e);
}

void linkLast(E e) {
        //定义l节点,暂存尾节点
        final Node<E> l = last;

        //创建新的节点元素,为目标值
        final Node<E> newNode = new Node<>(l, e, null);

        //尾节点赋值为新的节点元素
        last = newNode;

        //如果l节点为null,说明原来为空链表,即头指针也指向新的节点元素
        if (l == null)
            first = newNode;

        //如果不是null,说明原来不为空链表,即l节点的尾指针为新的
        else
            l.next = newNode;

        //调整大小和版本
        size++;
        modCount++;
    }

4.2.3 在指定位置添加add

  public void add(int index, E element) {
        //检查下标
        checkPositionIndex(index);

        //下标等于链表长度,即直接添加到尾部
        if (index == size)
            linkLast(element);
        else
        //否则添加到指定下标的前面
            linkBefore(element, node(index));
    }

void linkBefore(E e, Node<E> succ) {
        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++;
    }


4.4  删除

节点删除的方式和追加类似,我们可以选择从头部删除,也可以选择从尾部删除,删除操作会把节点的值,前后指向节点都置为 null,帮助 GC 进行回收。

4.4.1从头部删除

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

4.4.2从尾部删除

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

4.5查询

4.5.1根据下标查询

从源码中我们可以发现,LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,如果是后半部分,就从尾开始寻找。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能。

public E get(int index) {
        //判断下标位置
        checkElementIndex(index);
        return node(index).item;
    }

Node<E> node(int 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;
        }
    }

 

4.5.2查询第一个元素

 

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

4.5.3查询最后一个元素

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

4.6 迭代器


4.6.1 ListIterator

因为 LinkedList 要实现双向的迭代访问,所以我们使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。

Java 新增了一个迭代接口,叫做:ListIterator,如下图所示:

// 双向迭代器
private class ListItr implements ListIterator<E> {
    //上一次执行 next() 或者 previos() 方法时的节点位置
     private Node<E> lastReturned;

    //下一个节点
     private Node<E> next;

    //下一个节点的位置
     private int nextIndex;

     //expectedModCount:期望版本号;modCount:目前最新版本号
     private int expectedModCount = modCount;
}
我们先来看下从头到尾方向的迭代:
// 判断还有没有下一个元素
public boolean hasNext() {
    // 下一个节点的索引小于链表的大小,就有
     return nextIndex < size;
}

// 取下一个元素
public E next() {
     //检查期望版本号有无发生变化
     checkForComodification();

     if (!hasNext())//再次检查
         throw new NoSuchElementException();

     // next 是当前节点,在上一次执行 next() 方法时被赋值的。
     // 第一次执行时,是在初始化迭代器的时候,next 被赋值的
     lastReturned = next;

     // next 是下一个节点了,为下次迭代做准备
     next = next.next;

     nextIndex++;

 return lastReturned.item;
}

4.6.2迭代器和for循环效率差异

 

 LinkedList<Integer> linkedList = new LinkedList<>();
        for(int i = 0 ; i < 100000; i++){
            linkedList.add(i);
        }
        long beginTimeFor = System.currentTimeMillis();
        for(int i = 0 ; i < 100000 ; i++){
            System.out.print(linkedList.get(i));
        }
        long endTimeFor = System.currentTimeMillis();
        System.out.println("使用普通for循环遍历100000个元素需要的时间:"+ (endTimeFor - beginTimeFor));


        long beginTimeIte = System.currentTimeMillis();
        Iterator<Integer> it = linkedList.listIterator();
        while(it.hasNext()){
            System.out.print(it.next()+" ");
        }
        long endTimeIte = System.currentTimeMillis();
        System.out.println("使用迭代器遍历100000个元素需要的时间:"+ (endTimeIte - beginTimeIte));

我们可以看到10万的数据,遍历的时间已经相差很大了,如果数据量更大,时间可能相差更多的,那是为什么造成这种情况呢

1.采用for循环走的是LinkedList的get方法,这实际是一个简单的二分查询,每查询一个元素,都要走二分查询,那随着查询的数据越来越多,很大的一部分时间都浪费在查询上面,每个都重复查询之前的步骤,所以需要的时间多。

2.采用迭代器,则不是重复查询,因为其记录了上次查询的位置,所以需要的时间少。

总结

本文基于java8从源码分析LinkedList时如何构建的,底层的数据结构包括哪些,同时对多个方法逐步分析,包括添加add,删除remove,查询get等操作,还分析了迭代器与传统for循环相比,有哪些优势,为什么会产生优势等,若有不对之处,请批评指正,望共同进步,谢谢!

拜拜咯。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值