目录
一、依赖关系
LinkedList是一种基于链表实现的List集合,继承和实现关系如下:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}
1、AbstractSequentialList类、AbstractList类
AbstractSequentialList类是LinkedList类的抽象父类,同时它又继承了AbstractList类,AbstractSequentialList类中通过列表迭代器ListIterator 实现了get()、set()、add()、remove()等功能,为LinkedList集合按次序访问元素提供了支持,而AbstractList类则提供了随机访问的支持,像ArrayList、Vector等具有随机访问特性的集合则直接继承了AbstractList类。
2、List接口、Deque接口
实现了List接口,说明LinkedList类具备了List集合通用的特性,比如可以对集合元素进行添加add、删除remove、获取get、修改set、排序sort、列表迭代listIterator等基本操作。实现的Deque接口,是一个双端队列接口,说明LinkedList类具备了Deque队列通用的特性,比如可以从 队列头部 或 队列尾部 对集合元素进行添加(如add/offer/push),删除(如remove/poll/pop),获取(如get/peek),迭代Iterator等基本操作。
3、List集合的依赖关系
从List集合的实现类开始,向上溯源,画出List集合的依赖关系:
二、LinkedList结构分析
链表是以节点的方式来存储数据,它是一种线性表,但属于链式存储。而节点是由数据域(data)+ 指针域(next)组成的,链表在内存的空间并不是连续的,每个节点通过指针域来指向下一个节点。链表有带头节点的链表和无头节点的链表之分,也有单向链表和双向链表之分。
Java中的LinkedList是基于链表实现的List集合,它到底是什么样的结构呢?相关的源码分析如下:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
/** LinkedList集合大小 */
transient int size = 0;
/** 节点内部类 */
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;
}
}
/** 节点方法 -- 返回指定元素索引处的(非空)节点 */
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;
}
}
/** 链表头节点 */
transient Node<E> first;
/** 链表尾节点 */
transient Node<E> last;
/** LinkedList构造器 */
public LinkedList() {}
}
从上面代码分析中,我们可以总结出:
- LinkedList的链表是由节点Node组成的,节点Node内包含了三个变量:元素item、指向上一个节点的引用prev 、指向下一个节点的引用 next,说明LinkedList是一个双向链表结构;
- node()方法用于遍历链表查找出目标节点的操作,比如在指定位置插入元素、删除指定位置上的元素、修改指定位置上的元素、获取元素等操作,当查询的索引index小于链表节点数量的一半时,说明查找的节点位于链表的前半部分,采取从前往后遍历的方式折半查找,反之,采取倒序遍历方式进行折半查找,说明LinkedList的查询效率慢,最差的情况下会遍历链表一半的节点;
- 拥有的无参构造器不初始化成员,说明初始的LinkedList不包含任何节点;
三、LinkedList增删与查询操作的性能分析
上面分析了node()方法后发现,LinkedList集合的增删可分为单纯的添加或删除元素操作,比如:public boolean add(E e) {...} 或 public boolean remove(Object o) ,它们不涉及到节点位置的查询;又分为在指定节点位置上进行添加和删除元素操作,比如:public void add(int index, E element) {...} 或 public E remove(int index) {...},因此,为了便于分析LinkedList集合的增删操作和查询操作的性能,这里我尚且将涉及到节点位置查询的操作都视为查询操作了。
LinkedList集合的增删和查询操作,源码分析如下:(注意 —— 这里以List接口内的增删查API为例,而Deque接口内的增删查API,后面再进行对比整理)
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
/** 链表头节点 */
transient Node<E> first;
/** 链表尾节点 */
transient Node<E> last;
/** add(E e):向集合中追加一个元素 */
public boolean add(E e) {linkLast(e); return true;}
/** add(int index, E element):向集合指定位置插入一个元素,多了几步对索引的检查 */
public void add(int index, E element) {
checkPositionIndex(index); // 越界检查
if (index == size)
linkLast(element); // 如果index等于集合的大小,追加元素
else
linkBefore(element, node(index)); // 否则,根据index查找到该节点后,在该节点附近创建新节点存储元素
}
void linkLast(E e) {
final Node<E> l = last; // 假设存在一个节点
final Node<E> newNode = new Node<>(l, e, null); // 创建存储该元素e的新节点
last = newNode;
if (l == null)
first = newNode; // 假设的节点不存在,新节点成了头节点
else
l.next = newNode; // 假设的节点存在,该节点的next引用会指向新节点
size++; // 表示集合数量+1
modCount++; // 修改统计计数器+1
}
/** 在succ节点之前,通过改变节点引用关系创建新的节点,在新节点内存放元素 */
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev; // succ为后节点, pred为前节点
final Node<E> newNode = new Node<>(pred, e, succ); // 新节点
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
/**
* remove(Object o):删除集合中的一个元素,
* 注意 LinkedList集合是可以存储多个null元素的,也可以存储多个相同的元素。
**/
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) { // 从头开始遍历,找到第一个存在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;
}
/** 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;
if (prev == null) { // 删除指向前一节点的引用
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) { // 删除指向后一节点的引用
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null; // 删除该节点元素
size--; // 表示集合数量-1
modCount++;
return element;
}
/** 查询集合元素 */
public E get(int index) {
checkElementIndex(index);
return node(index).item; // 元素是存放在节点内的item变量上的
}
/** 修改集合元素 */
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index); // 要修改先找到具体节点
E oldVal = x.item;
x.item = element; // 然后替换节点内的元素
return oldVal;
}
}
先来看看查询操作的过程,一开始根据查询索引index,调用node()方法确定节点位置,而node()方法内会采用二分法选择不同的遍历查找策略,最坏情况下会遍历链表一半的节点,当LinkedList集合非常大时,查询效率还是比较慢的;
再来看看如何添加或删除一个元素的操作,元素是存储在节点内的,因此操作元素一定要通过操作节点实现。如果直接在集合内追加一个元素,只需要将前一节点的next引用指向新节点(前提是集合不为空),即可完成追加操作;如果在集合内的某一位置插入元素,要先找到该位置上对应的节点(势必会调用node()方法遍历查找了,为了方便分析,这里我称为After节点),这样也能确定After节点的前一个节点Before节点,然后创建一个存储插入元素的新节点New节点,最后将Before节点的next引用指向New节点,将After节点的prev引用指向New节点,就能实现断开连接After节点和Before节点,插入New节点。通过改变前后节点与新节点的引用关系,即可完成插入操作;删除的思路类似,本质上都是通过改变节点间引用关系,实现了快速添加或删除元素的功能。
前一篇的ArrayList集合源码分析提到过,ArrayList集合基于随机访问接口和数组下标定位元素等特性,在大数据量操作的场景中,查询操作效率远高于增删操作。而LinkedList集合则基于改变节点间的引用关系快速实现增删元素的特点,用在大数据量操作的场景中,增删操作效率远高于查询操作。因此是使用ArrayList集合还是使用LinkedList集合,需要根据实际情况选择。
四、增删改查API对比整理
之前提到的增删改查的源码分析均是基于List接口的,接下来加上Deque接口(利用双端队列的出队和入队,实现集合的头元素和尾元素操作),综合盘点一下:
添加操作
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
/** 向集合末尾追加一个元素 */
public boolean add(E e) {linkLast(e);return true;}
/** 向集合指定位置插入一个元素 */
public void add(int index, E element) {...}
/** 求LinkedList集合的并集 */
public boolean addAll(Collection<? extends E> c) {return addAll(size, c);}
public boolean addAll(int index, Collection<? extends E> c) {...}
/** 向集合开头插入一个元素 */
public void addFirst(E e) {linkFirst(e);}
/** 向集合末尾追加一个元素,等同于add(E e) */
public void addLast(E e) {linkLast(e);}
/** 向集合末尾追加一个元素,等同于add(E e) */
public boolean offer(E e) {return add(e);}、
/** 向集合开头插入一个元素,等同于addFirst(E e) */
public boolean offerFirst(E e) {addFirst(e);return true;}
/** 向集合末尾追加一个元素,等同于addLast(E e) */
public boolean offerLast(E e) {addLast(e);return true;}
/** 向集合开头插入一个元素,等同于addFirst(E e) */
public void push(E e) {addFirst(e); }
删除操作
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
/** 删除集合内的某个元素 */
public boolean remove(Object o) {...}
/** 删除指定位置的元素 */
public E remove(int index) {checkElementIndex(index);return unlink(node(index));}
/** 删除集合内第一个元素,集合为空,会抛NoSuchElementException */
public E removeFirst() {final Node<E> f = first; ...; return unlinkFirst(f);}
/** 删除集合内最后一个元素,集合为空,会抛NoSuchElementException */
public E removeLast() {final Node<E> l = last; ...; return unlinkLast(l);}
/** 删除集合内第一个元素,等同于removeFirst() ,集合为空,会抛NoSuchElementException */
public E remove() {return removeFirst();}
/** 删除集合内的某个元素,正序遍历后删除第一个出现的元素,等同于remove(Object o) */
public boolean removeFirstOccurrence(Object o) {return remove(o);}
/** 删除集合内的某个元素,倒序遍历后删除第一个出现的元素 */
public boolean removeLastOccurrence(Object o) {...}
/** 删除集合内第一个元素,集合为空,不会抛异常 */
public E poll() {final Node<E> f = first;return (f == null) ? null : unlinkFirst(f);}
/** 删除集合内第一个元素,集合为空,不会抛异常 */
public E pollFirst() {final Node<E> f = first;return (f == null) ? null : unlinkFirst(f);}
/** 删除集合内最后一个元素,集合为空,不会抛异常 */
public E pollLast() {final Node<E> l = last;return (l == null) ? null : unlinkLast(l);}
/** 删除集合内第一个元素,集合为空,会抛NoSuchElementException */
public E pop() {return removeFirst();}
}
查询操作
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
/** 查询集合内指定位置元素 */
public E get(int index) {checkElementIndex(index);return node(index).item;}
/** 查询集合内第一个元素,集合为空,会抛NoSuchElementException */
public E getFirst() {final Node<E> f = first;...;return f.item;}
/** 查询集合内最后一个元素,集合为空,会抛NoSuchElementException */
public E getLast() {final Node<E> l = last;...;return l.item;}
/** 查询集合内第一个元素,集合为空,不抛异常 */
public E peek() {final Node<E> f = first;return (f == null) ? null : f.item;}
/** 查询集合内第一个元素,集合为空,不抛异常 */
public E peekFirst() {final Node<E> f = first;return (f == null) ? null : f.item;}
/** 查询集合内最后一个元素,集合为空,不抛异常 */
public E peekLast() {final Node<E> l = last;return (l == null) ? null : l.item;}
/** 查询集合内第一个元素,集合为空,会抛NoSuchElementException */
public E element() {return getFirst();}
}
五、迭代器
List集合中有两种迭代器:一种是通过iterator()方法获取的原生迭代器Iterator,另一种是通过listIterator()方法获取的列表迭代器ListIterator,关于二者的比较在ArrayList集合源码分析中也分析过了,可以参考:【JDK源码】ArrayList源码分析。
列表迭代器ListIterator
在ArrayList集合中,定义了一个内部类ListItr,该类实现了ListIterator接口,底层通过数组的方式实现了自己的next()、previous()、set()、add()、remove()、forEachRemaining() 等迭代器功能,而LinkedList集合也是如此,只是通过链表方式实现的而已。
原生迭代器Iterator
在ArrayList集合中,定义了一个内部类Itr,该类实现了Iterator接口,底层通过数组的方式只实现了next()、remove()、forEachRemaining() 等迭代器功能,而LinkedList集合则有所不同,定义的内部类DescendingIterator 实现了Iterator接口,这里使用到了适配器设计模式,适配器通过列表迭代器的previous属性,实现了一种降序功能的迭代器DescendingIterator,且只提供了next()、hasNext()、remove()等迭代方法,实现的源码如下:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
public Iterator<E> descendingIterator() {
return new DescendingIterator();
}
private class DescendingIterator implements Iterator<E> {
private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}
}
当然不得不提一下ConcurrentModificationException异常,它是foreach遍历时使用集合原生的删除方法造成的,原因在ArrayList集合源码分析中也分析过了,这里提醒一下,使用LinkedList集合进行遍历删除时也要规避这样的问题,即使用迭代器提供的remove()方法。
六、分割器
分割器Spliterator是Java8引入的一种保证并发安全操作集合或Map的接口,集合或Map按照分割规则,分割成多段,在多线程中能得到安全的执行。在ArrayList集合源码分析中,对该接口及其实现类ArrayListSpliterator做了详细而深入的分析,这里不再赘述了,可以参考:【JDK源码】ArrayList源码分析。
换汤不换药,LinkedList集合提供的LLSpliterator类,与ArrayListSpliterator类对比后可以看到,API的实现思路是一样的,保证并发安全操作的原理也是一样的,只不过一个是链表方式,一个是数组方式。
小结
至此,LinkedList集合重点的内容基本整理和分析完了,作为List集合的一种类型,有很多地方的特性和ArrayList集合极为相似,同时又有所区别,均在上面进行了对比说明。深入源码分析是不简单的事情,虽然耗费时间,但在这个过程中,确实能发现和收获不少的东西,比如LinkedList的结构是什么样的,为什么说LinkedList集合的增删操作效率高于查询操作,为什么LinkedList集合的降序迭代器能实现降序的功能,为什么LinkedList集合的分割器操作是线程安全的等等,学习的路上贵在坚持,不积硅步无以至千里,点滴付出终将有所收获,共同进步吧~