Java集合——LinkedList源码详解

0. 前言

先对LinkedList的特性进行一个概述: 
1LinkedList底层实现为双向循环链表。链表的特点就是插入删除数据快,而查询数据慢

2)因为使用链表的原因,所以不存在容量不足的问题,没有扩容机制

3)从后面的源码分析中我们也可以看出,LinkedList支持null并且LinkedList没有同步机制

4LinkedList直接继承于AbstractSequentialList,同时实现了List接口,也实现了Deque接口。

AbstractSequentialList顺序访问的数据存储结构提供了一个骨架类实现,如果要支持随机访问,则优先选择AbstractList类继承。LinkedList 基于链表实现,因此它继承了AbstractSequentialList。本文原创,转载请注明出处:Java集合——LinkedList源码详解_SEU_Calvin的博客-CSDN博客

1.  LinkedList数据存储格式

上面也提到了,LinkedList底层实现为双向链表,下面是某个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;
     }
}

下面我们分析一个LinkedListadd()操作,可以帮助我们理解LinkedList的双向链表的实现原理。

2.  LinkedListadd操作

public void add(int index, E element) {
    checkPositionIndex(index);
    if (index == size)
       linkLast(element);
    else
       linkBefore(element, node(index));
}

上面是根据index进行的LinkedListadd操作,首先会判断index是否合法,再判断是不是将该节点插入到链表的最后,最后才是进行链表的中间插入操作。

2.1    链表尾部add

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

linkLast()方法中,首先会生成一个新的Node节点,然后将这个新的末尾节点赋值给last节点,如果lnull,说明是第一次加入数据,就将这个节点置为first节点,代表链表中第一个有数据的节点,否则将上一个节点的next指向这个节点。

说到第一个节点和最后一个节点,LinkedList提供给了我们具体的方法进行这两个特殊位置的数据获取:

//获取第一个元素
System.out.println("第一个元素是:" + list.getFirst());  
//获取最后一个元素
System.out.println("最后一个元素是:"+list.getLast());  

2.2    链表中间add

中间插入节点调用了linkBefore(element,node(index))方法,那么首先分析一下node(index)的逻辑:

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

首先判断index值是不是小于整个链表长度的一半,整个if/else逻辑是在判断要插入的位置是距离链表头近还是链表尾近,目的更快的找到原来index处的节点并返回。接下来就是linkBefore()方法:

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

pred 是当前要插入位置节点的上一个节点,即图中的第一个节点。newNode 将要插入的对象包装成node节点(指定上一个节点为pred,下一个节点为node()返回值succ)

接着将succ节点的上一个节点指定为我们的新节点newNode。那么肯定会有逻辑将prednext设置为newNode

果然,最后做了一个判断,如果prednull,说明succ节点为第一个有数据的节点,就将生成的新节点newNode置为first节点,否则指定上个节点的下一个节点为生成的新节点,即pred.next = newNode

这样就完成了整个链表数据插入过程。显然是不同于ArrayList那样地进行数组复制。

3.  LinkedListget操作

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

get操作就比较简单了,对链表进行遍历,直接找到node节点,返回item数据即可。

4.  LinkedList既可以用作队列,又可以用作栈

LinkedList<Integer> list = new LinkedList<>();
//两种入队列方式,插入队尾
list.add(1);
list.offer(2);
System.out.println(list.peek()); // 2->1  打印1
//poll()出队首
System.out.println(list.poll()); // 2     打印1 剩下2
System.out.println(list.peek()); // 2     打印2 剩下2
list.offer(3); // 3->2
System.out.println(list.peek()); // 3->2  打印2 剩下3、2

//pop是移出队首,出栈api
System.out.println(list.pop()); // pop出2,打印2,剩下3
//push直接插入队首
list.push(4); //3->4
System.out.println(list.peek()); // 3->4  打印4,剩下3、4
System.out.println(list.pop()); // pop出4,打印4,剩下3
System.out.println(list.peek()); //       打印3,剩下3

看上述代码,add()和offer()都是将元素插入队尾,peek()查看的永远是队首元素。poll()完成队列的出队操作,也就是移除peek()到的元素。而栈数据结构用到的两个api,一个push()、一个pop(),前者是将元素插入到链表头,也就是队首,这样pop()只需要移出队首,即可完成后进先出的特性。两套队列和栈的API一般不会像demo里那样混用,但是这个demo可以让你更好的理解api底层是怎么实现的。

5.  ArrayListLinkedList的比较

5.1    ArrayListLinkedList的相同点

1)两者均不是线程安全的。

2)两者都支持null值。

3)都实现了List接口。

 5.2  ArrayList和LinkedList的不同点

1LinkedList 基于链表实现,便于顺序访问,它继承了AbstractSequentialList。而ArrayList支持随机访问,继承了AbstractList类。

2)因为LinkedList 是基于链表的,因此不像ArrayList需要扩容机制

3)各种操作的性能对比:对于ArrayList来说,得益于快速随机访问的特性,获取任意位置元素是比较有效率的。如果是add或者remove操作,要分两种情况,如果是在ArrayList尾部add,是不需要移动其他元素,耗时是O(1)。但如果在中间插入新元素的话,耗时是O(n-index)。另外,当ArrayList扩容时,会自动生成一个新的array(长度是之前的1.5),再将旧的array移值上去,耗时是O(n)。为了确保本文正确性,博主会在发现某部分有不妥描述时及时修改,请确保您看到的是原文,原文链接为SEU_Calvin的博客

因此,ArrayListget操作快一些,而add操作,若add的位置为List中间,肯定是LinkedList要快一些,尾部的话两者差不多。具体使用哪个需要分场景选择最合适的。

最后希望这篇文章对你有所帮助,也希望大家多点赞支持。 ╮( ̄▽ ̄”)╭

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值