一步一图带你深入理解LinkedList底层原理

上一篇文章《一步一图带你深入理解ArrayList底层原理》中,我们介绍了ArrayList的底层原理,除了ArrayList之外,还有个集合我们也经常用到,就是LinkedList,它与ArrayList有什么区别呢?本篇文章就来讲讲它内部的实现原理,看完你就知道有什么不同了。

特性

  • 基于双向链表实现,添加删除元素快O(1),查找元素慢 O(n)
  • 实现了ListQueue接口的方法,允许所有的元素,包括null

初始变量

// 记录链表长度
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结构如下图所示:
在这里插入图片描述
包含了3个要素:
(1)当前节点的数据 item
(2)前置节点的引用 prev,存放前置节点的地址
(3)后置节点的引用 next,存放后置节点的地址

初始化

  • new LinkedList() :初始化一个空的双向链表集合
    初始化的时候,链表的头节点first和尾节点last都为null

add方法

public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
	// 1. 获取尾节点
    final Node<E> l = last;
    // 2. 新建节点,节点的prev指向尾节点,next为null,元素为e
    final Node<E> newNode = new Node<>(l, e, null);
    // 3. 将新建的节点标记为尾节点
    last = newNode;
    if (l == null)
    	// 4. 如果是第一次添加,last为 null,将新建的节点同时标记为头节点
        first = newNode;
    else
    	// 5. 不是第一次添加,设置尾节点的next为当前新建节点 
        l.next = newNode;
    size++;
    modCount++;
}

从源码看到,add方法是在链表尾部追加数据节点。主要步骤如下:
(1)第一次往链表中添加节点时,会新建一个节点,头指针first和尾指针last都指向该节点
在这里插入图片描述
(2)第二次往链表中添加节点时,会将last指针指向新加的节点,头节点的next指向新节点,新节点的prev指向头节点
在这里插入图片描述
(3)第三次往链表中添加节点时,会再将last指针指向新加的节点,原来的last指向的节点的next指向新加的节点,新节点的prev指向原来的last节点
在这里插入图片描述
(4)依次类推,每次添加节点,都会将尾节点指针后移,同时设置新节点的prev指向原来的尾节点,原来尾节点的next指向新节点

remove方法

public E remove() {
    return removeFirst();
}
public E removeFirst() {
	// 获取头结点,头结点为null,抛出异常
    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;
    // 1.获取头结点元素
    final E element = f.item;
    // 2.获取头结点的next结点
    final Node<E> next = f.next;
    // 3.将头结点的item和next设置为null
    f.item = null;
    f.next = null; // help GC
    // 4.将头结点的后置节点标记为新的头节点
    first = next;
    if (next == null)
    	// 如果链表只有一个元素,头节点next为null,移除头节点后,链表为空,设置last也为null
        last = null;
    else
    	// 移除头节点后,将头节点的后置节点的prev设置为null
        next.prev = null;
    size--;
    modCount++;
    return element;
}

从源码中看到,remove方法是移除链表的头部节点
(1)链表中只有一个元素的时候执行remove方法,将item,first,last都设置为了null
在这里插入图片描述
(2)链表中至少有两个元素的时候执行remove方法,将first指针后移,头节点没有引用,元素值被设置为null,从而达到移除链表元素的
在这里插入图片描述

pop方法

public E pop() {
    return removeFirst();
}

原理同remove方法一样,都是从链表头部移除元素

poll方法

public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

原理同remove方法一样,都是从链表头部移除元素

peek方法

public E peek() {
   	final Node<E> f = first;
    return (f == null) ? null : f.item;
}

获取链表头部元素,如果元素为null,则返回null

push方法

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

在链表头部添加元素

线程安全性

我们来看一段代码

public static void main(String[] args) throws InterruptedException {
    List<String> list = new LinkedList<>();
    CountDownLatch countDownLatch = new CountDownLatch(5);
    ExecutorService executors = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 5; i++) {
        executors.submit(() -> {
            for (int k = 0; k < 1000; k++) {
                list.add(Thread.currentThread().getName() + k);
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    System.out.println(list.size());
}
=========结果=========
4939

代码里我们起了5个线程,每个线程添加1000个元素,正常情况下我们最后输出的数组长度应该为 5000,可实际上我们打印出来的值却比5000要小。
从结果上看,add方法不是线程安全的
通过源码我们能看到,LinkedList操作元素的方法都没有进行同步处理,线程A和线程B并发的往链表中的添加节点,存在一种情况:线程A和线程B获取到的尾节点都是同一个,执行添加操作的时候,线程A会将线程B的添加的元素给覆盖掉,导致数据缺失。

总结

1.基于双向链表结构,查找慢,添加删除元素快
2.操作元素线程不安全
3.提供了一系列添加和删除元素的方法

添加元素

  • add:在链表尾部添加元素,添加成功返回true
  • add(index,element):在指定索引位置添加元素,索引位置不在链表范围内,则抛出越界异常IndexOutOfBoundsException
  • addFirst:在链表头部添加元素,没有返回值
  • push:在链表头部添加元素,没有返回值
  • offer:在链表头部添加元素,添加成功返回true
  • offerLast:在链表尾部添加元素,添加成功返回true

删除元素

  • remove:删除并返回链表头部元素;如果元素为null,抛出NoSuchElementException异常
  • remove(index):移除指定索引位置的元素,索引不在链表范围内,抛出越界异常IndexOutOfBoundsException
  • removeLast:删除并返回链表尾部元素,如果元素为null,抛出NoSuchElementException异常
  • pop:删除并返回链表头部元素;如果元素为null,抛出NoSuchElementException异常
  • poll:删除并返回链表头部元素;如果元素为null,则返回null
  • pollLast:删除并返回链表尾部元素;如果元素为null,则返回null

获取元素

  • peek:获取链表头部元素,如果元素为null,则返回null
  • getFirst:获取链表头部元素,如果元素为null,则抛出NoSuchElementException异常
  • getLast:获取链表尾元素,如果元素为null,则抛出NoSuchElementException异常
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值