一、什么是LinkedList
LinkedList和Vector以及ArrayList一样都是一个集合,实现了List接口
二、LinkedList数据结构分析
首先在给大家分析源码之前我们要了解一点,就是LinkedList的底层是什么,我们都知道Vector以及ArrayList的底层都是数组,可能有些小伙伴以为LinkedList的底层也是数组,但是结果就是LinkedList的底层并不是数组,让我带大家看一下LinkedList的底层是什么吧!
首先我们先看下LinkedList源码里的属性:
transient int size = 0;
// 这个链表的首节点
transient Node<E> first;
// 这个链表的尾结点
transient Node<E> last;
我想很多小伙伴第一眼就发现了这个Node是什么?让我们看一下LinkedList源码里怎么写的
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节点类;
item:代表当前节点的值;
next:下一个节点的值;
prev:上一个节点值;
从这个图我们能更直观的看出来为什么是双向的;在这里我就是稍微带大家了解一下这个双向链表的结构,方便大家更好的了解LinkedList的源码
三、LinkedList源码分析
前面让大家知道了LInkedList集合的底层其实是一个双向链表那么我现在就给大家通过源码分析一下常用的方法。(LinkedList不需要扩容方法因为是一个链表,因为这个构造函数没什么说的所以我也不写了…绝对不是懒)
// 我们以最常用的add方法开始分析
public boolean add(E e) {
// 在这里我们可以看出add方法实际上是调用linkLast()这个方法
linkLast(e);
return true;
}
// 通过名字我们看出是添加尾结点的元素
void linkLast(E e) {
// last是指链表的尾结点
final Node<E> l = last;
/*
而这个newNode这个节点:
prev = null;
item = e;
next = null
*/
final Node<E> newNode = new Node<>(l, e, null);
// 把这个新节点赋值给last这个节点
last = newNode;
// 看是不是第一次添加
if (l == null)
// 如果是新添加的节点那么首尾节点都是这个节点
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
然后我们来分析一下第二次添加:
void linkLast(E e) {
final Node<E> l = last;
/*
这里我们做一个假设就是第一次添加的是1,第二次添加的是2
那么这个newNode就是:
prev:
prev:null
item:1
next:null
item:2
next:null
*/
final Node<E> newNode = new Node<>(l, e, null);
// 在这里我们就把新的newNode的值赋给了last
last = newNode;
// 因为这里不是null,所以进入else
if (l == null)
first = newNode;
else
/*
那么我们再分析一下这个l节点
prev:null
item:1
next: newNode(这个节点):
prev: l(这个节点)
item:2
next:null
*/
l.next = newNode;
size++;
modCount++;
}
可能我这种分析方法有些小伙伴看的可能有点迷惑,那么我就用语言来说明一下:
- final Node l = last;因为last是链表的尾结点就是在未添加节点之前这个链表的最后一个节点
- final Node newNode = new Node<>(l, e, null);在这里我们创建一个新的节点其中prev = f;item = e;next=null(这里为末端添加所以下一个节点一定是null)
- last = newNode;将这个新节点赋值给last也就是告诉我们这个就是最新的尾结点
- if (l == null)首先要判断是不是第一次添加
- first = newNode;如果是第一次添加就把这个节点赋值给first也就是这个链表的首节点
- l.next = newNode;如果不是就把l也就是之前的尾结点的next指向这个新的节点
接下来我们在分析一下在首节点添加,因为是双向链表所以可以双向添加:
private void linkFirst(E e) {
// 获取首节点并保存到f这个节点中
final Node<E> f = first;
// 根据需要添加的值创建节点,因为是首节点添加所以,prev肯定是null
final Node<E> newNode = new Node<>(null, e, f);
// 把这个创建好的节点赋值给首节点
first = newNode;
// 看是不是第一次添加
if (f == null)
// 是的话首尾节点都是这个新节点
last = newNode;
else
// 不是的话就把f的上个节点也就是prev指向了newNode
f.prev = newNode;
size++;
modCount++;
}
这里大家可以根据我上面分析的尾结点添加来看,其实就是一样的所以这里我就不做详细分析了;
然后分析几个简单的源码:
// 获取该索引下节点的值
public E get(int index) {
// 这个就是抛出个异常没啥好说的
checkElementIndex(index);
return node(index).item;
}
// 修改该索引下节点的值
public E set(int index, E element) {
checkElementIndex(index);
// 首先获取这个索引下的节点
Node<E> x = node(index);
// 获取该节点旧值
E oldVal = x.item;
// 新值赋值给该节点
x.item = element;
return oldVal;
}
// 我们分析一个这个添加,或者说插入
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);
// 将这个新节点保存给该索引节点的prev也就是前一个节点
succ.prev = newNode;
// 首先要看这个节点的前一个节点是否存在
if (pred == null)
// 如果不存在就是首节点
first = newNode;
else
// 如果存在就把新节点赋值给pred的下一个节点
pred.next = newNode;
size++;
modCount++;
}
// 最后说一下这个remove移除方法
public boolean remove(Object o) {
// 这里看传入的参数是否为null
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
// 这里就是移除第一为null的节点
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
// 这里就是移除参数值所在的节点
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
四、LinkedList的优点和不足
其实说是LinkedList的优势不如说是链表的优势,链表的优势在于插入、删除的效率快但是查询慢,我们通过源码可以看出插入删除的时间复杂度是O(1),而查询的时间复杂度是O(n)