一、引言
1.概念
- LinkedList是基于链表实现的,所以先讲解一下什么是链表。链表原先是C/C++的概念,是一种线性的存储结构,意思是将要存储的数据存在一个存储单元里面,这个存储单元里面除了存放有待存储的数据以外,还存储有其下一个存储单元的地址(下一个存储单元的地址是必要的,有些存储结构还存放有其前一个存储单元的地址),每次查找数据的时候,通过某个存储单元中的下一个存储单元的地址寻找其后面的那个存储单元。
- 理解:
- LinkedList是一个双向链表
- 也就是说list中的每个元素,在存储自身值之外,还 额外存储了其前一个和后一个元素的地址,所以 也就可以很方便地根据当前元素获取到其前后的元素
- 链表的尾部元素的后一个节点是链表的头节点;而链表的头结点前一个节点则是则是链表的尾节点(是不是有点像贪吃蛇最后 头吃到自己尾巴的样子,脑补下)
- 既然是一个双向链表,那么必然有一个基本的存储单元,让我们来看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;
}
}
图示:
2.集合要点
要点 | 说明 |
---|---|
是否可以为空 | 可以 |
是否有序 | 有序 |
是否可以重复 | 允许重复 |
是否线程安全 | 线程不安全 |
二、分析
1.类的继承结构图
2.属性
// LinkedList节点个数
transient int size = 0;
/**
* Pointer to first node. 指向头结点
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node. 指向尾节点
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
3.构造函数
3.1空参构造函数
public LinkedList() {
}
3.2 collection参数构造函数
public LinkedList(Collection<? extends E> c) {
this();
// 将集合添加到链表中去
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
// 从链表尾巴开始集合中元素
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
// 1.添加位置的下标的合理性检查
checkPositionIndex(index);
// 2.将集合转换为Object[]数组对象
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
// 3.得到插入位置的前继节点和后继节点
Node<E> pred, succ;
if (index == size) {
// 从尾部添加的情况:前继节点是原来的last节点;后继节点是null
succ = null;
pred = last;
} else {
// 从指定位置(非尾部)添加的情况:前继节点就是index位置的节点,后继节点是index位置的节点的前一个节点
succ = node(index);
pred = succ.prev;
}
// 4.遍历数据,将数据插入
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// 创建节点
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
// 空链表插入情况:
first = newNode;
else
// 非空链表插入情况:
pred.next = newNode;
// 更新前置节点为最新插入的节点(的地址)
pred = newNode;
}
if (succ == null) {
// 如果是从尾部开始插入的,则把last置为最后一个插入的元素
last = pred;
} else {
// 如果不是从尾部插入的,则把尾部的数据和之前的节点连起来
pred.next = succ;
succ.prev = pred;
}
size += numNew; // 链表大小+num
modCount++; // 修改次数加1
return true;
}
链表的数据结构大概就是这样的:
所有的元素操作都是在这样的数据结构上进行操作的,可以大概的脑补下,就很容易理解了。
因为LinkedList即实现了List接口,又实现了Deque接口,所以LinkedList既可以添加将元素添加到尾部,也可以将元素添加到指定索引位置,还可以添加添加整个集合;另外既可以在头部添加,又可以在尾部添加。所以下面分list接口实现和deque接口实现来分析。
4.List接口
4.1 add(E e)方法
// 作用:将元素添加到链表尾部
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last; // 获取尾部元素
final Node<E> newNode = new Node<>(l, e, null); // 以尾部元素为前继节点创建一个新节点
last = newNode; // 更新尾部节点为需要插入的节点
if (l == null)
// 如果空链表的情况:同时更新first节点也为需要插入的节点。(也就是说:该节点既是头节点first也是尾节点last)
first = newNode;
else
// 不是空链表的情况:将原来的尾部节点(现在是倒数第二个节点)的next指向需要插入的节点
l.next = newNode;
size++; // 更新链表大小和修改次数,插入完毕
modCount++;
}
4.2 add(int index, E element)方法
// 作用:在指定位置添加元素
public void add(int index, E element) {
// 检查插入位置的索引的合理性
checkPositionIndex(index);
if (index == size)
// 插入的情况是尾部插入的情况:调用linkLast()解释如上。
linkLast(element);
else
// 插入的情况是非尾部插入的情况(中间插入):linkBefore()见下面。
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev; // 得到插入位置元素的前继节点
final Node<E> newNode = new Node<>(pred, e, succ); // 创建新节点,其前继节点是succ的前节点,后接点是succ节点
succ.prev = newNode; // 更新插入位置(succ)的前置节点为新节点
if (pred == null)
// 如果pred为null,说明该节点插入在头节点之前,要重置first头节点
first = newNode;
else
// 如果pred不为null,那么直接将pred的后继指针指向newNode即可
pred.next = newNode;
size++;
modCount++;
}
4.3 get(int index)方法
public E get(int index) {
// 元素下表的合理性检查
checkElementIndex(index);
// node(index)真正查询匹配元素并返回
return node(index).item;
}
// 作用:查询指定位置元素并返回
Node<E> node(int index) {
// assert isElementIndex(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.4 remove(int index)方法
// 作用:移除指定位置的元素
public E remove(int index) {
// 移除元素索引的合理性检查
checkElementIndex(index);
// 将节点删除
return unlink(node(index));
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item; // 得到指定节点的值
final Node<E> next = x.next; // 得到指定节点的后继节点
final Node<E> prev = x.prev; // 得到指定节点的前继节点
// 如果prev为null表示删除是头节点,否则就不是头节点
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null; // 置空需删除的指定节点的前置节点(null)
}
// 如果next为null,则表示删除的是尾部节点,否则就不是尾部节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null; // 置空需删除的指定节点的后置节点
}
// 置空需删除的指定节点的值
x.item = null;
size--; // 数量减1
modCount++;
return element;
}
4.5 clear()方法
// 清空链表
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
// 进行for循环,进行逐条置空;直到最后一个元素
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
// 置空头和尾为null
first = last = null;
size = 0;
modCount++;
}
4.6 indexOf(Object o)
// 返回元素在链表中的索引,如果不存在则返回-1
public int indexOf(Object o) {
int index = 0;
// 如果元素为null,进行如下循环判断
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
// 元素不为null.进行如下循环判断
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
5. Duque接口
5.1 addFirst(E e)方法
// 作用:在链表头插入指定元素
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first; // 获取头部元素
final Node<E> newNode = new Node<>(null, e, f); // 创建新的头部元素(原来的头部元素变成了第二个)
first = newNode;
// 链表头部为空,(也就是链表为空)
if (f == null)
last = newNode; // 头尾元素都是e
else
f.prev = newNode; // 否则就更新原来的头元素的prev为新元素的地址引用
size++;
modCount++;
}
5.2 addLast(E e)方法
// 作用:在链表尾部添加元素e
public void addLast(E e) {
// 上面已讲解过,参考上面。add()方法
linkLast(e);
}
5.3 push(E e)方法
// 作用:往链表头部添加元素e
public void push(E e) {
addFirst(e);
}
5.4 getFirst()方法
// 作用:得到头元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
5.5 getLast()方法
// 作用:得到尾部元素
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
5.6 peek()方法
// 作用:返回头元素,并且不删除。如果不存在也不错,返回null
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
5.7 peekFirst()方法
// 作用:返回头元素,并且不删除。如果不存在也不错,返回null
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
5.8 peekLast()方法
// 作用:返回尾元素,如果为null,则就返回null
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
5.9 poll()方法
// 作用:返回头节点元素,并删除头节点。并将下一个节点设为头节点。
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
5.10 pollFirst()方法
// 作用:返回头节点,并删除头节点,并将下一个节点设为头节点。
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
5.11 pollLast()方法
// 作用:返回尾节点,并且将尾节点删除,并将尾节点的前一个节点置为尾节点
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
5.12 pop()方法
// 作用:删除头节点,如果头结点为null.则抛出异常
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
5.13 push(E e)方法
// 作用:将元素添加到头部
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
三、结尾
1.LinkedList和ArrayList的区别
- 顺序插入的速度ArrayList会快些,LinkedList的速度回稍慢一些。因为ArrarList只是在指定的位置上赋值即可,而LinkedList则需要创建Node对象,并且需要建立前后关联,如果对象较大的话,速度回慢一些。
- 基于上面的理解LinkedList的占用的内存空间要大一些。
- 数组遍历的方式ArrayList推荐使用for循环,而LinkedList则推荐使用foreach,如果使用for循环,效率将会很慢。(后面会有文章单独介绍,现在先记住结论)
- 一般我们这样认为:ArrarList查询和获取快,修改和删除慢;LinkedList修改和删除快,查询和获取慢。其实这样说不准确的。
LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Entry的引用地址;
ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址。
所以,如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,因为它是双向链表,所以在第2个元素后面插入一个数据和在倒数第2个元素后面插入一个元素在效率上基本没有差别,但是ArrayList由于要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList。
2.实际开发中怎么选择?
1、如果你十分确定你插入、删除的元素是在前半段,使用LinkedList
2、如果你十分确定你删除、删除的元素后半段,使用ArrayList
3、如果你上面的两点不确定,建议你使用LinkedList
说明:其一、LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;其二、插入元素的时候,弄得不好ArrayList就要进行一次扩容,而ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作,所以综合来看就知道选择哪个类型的list
3.版本说明
- 这里的源码是JDK8版本,不同版本可能会有所差异,但是基本原理都是一样的。
4.回顾和思考
- 还是那句话:再次思考学习集合源码的四个目标和上面的具体的内容,是不是有一种恍然大悟的感觉!!!