上一篇文章《一步一图带你深入理解ArrayList底层原理》中,我们介绍了ArrayList的底层原理,除了ArrayList之外,还有个集合我们也经常用到,就是LinkedList,它与ArrayList有什么区别呢?本篇文章就来讲讲它内部的实现原理,看完你就知道有什么不同了。
特性
- 基于双向链表实现,添加删除元素快O(1),查找元素慢 O(n)
- 实现了
List
和Queue
接口的方法,允许所有的元素,包括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
异常