基于JDK1.8对LinkedList集合的源码进行了深度解析,包括各种方法、链表构建、迭代器机制的底层实现,并且给出了与ArrayList集合的区别,以及如何使用LinkedList模拟栈和队列。
文章目录
1 LinkedList的概述
public class LinkedList< E >
extends AbstractSequentialList< E >
implements List< E >, Deque< E >, Cloneable, Serializable
JDK1.2的时候添加的集合类,底层是使用双向链表实现的线性表,能够从链表头部或者尾部操作数据元素,可用作双端队列,也可以模拟栈空间的储存。
直接继承了AbstractSequentialList抽象类,该类是链表实现的线性表的统一父类。对于随机访问数据(如数组)实现的线性表,应该直接继承AbstractList。AbstractSequentialList也继承AbstractList。
实现了List接口,因此也可以使用“索引”方法get(index)访问元素,但是效率较低,因为所有的get(index)实际上都是从头或尾开始顺序遍历的!此索引非数组的索引,而是自己维护的索引!
没有实现RandomAccess标志性接口,不支持快速随机访问。
实现了Cloneable, Serializable标志性接口,支持克隆和序列化!
JDK1.5时添加实现了Queue(队列)接口,JDK1.6时,改为直接实现Deque(双端队列)接口,提供了一批额外的方法(下面会讲到)!
此实现不是同步的。可以使用:List list = Collections.synchronizedList(new LinkedList(…))来转换成线程安全的List!
2 LinkedList的API方法
这里的API方法,是指除了List接口通用API方法之外的LinkedList特有的API方法。
其诞生之时(JDK1.2),针对链表的头和尾部提供了一系列方法:
public void addFirst(E e)
将指定元素插入此列表的开头。
public void addLast(E e)
将指定元素添加到此列表的结尾。
public E getFirst()
返回此列表的第一个元素。如果此列表为空,则抛出NoSuchElementException异常。
public E getLast()
返回此列表的最后一个元素。如果此列表为空,则抛出NoSuchElementException异常。
public E removeFirst()
移除并返回此列表的第一个元素。如果此列表为空,则抛出NoSuchElementException异常。
public E removeLast()
移除并返回此列表的最后一个元素。如果此列表为空,则抛出NoSuchElementException异常。
在JDK1.5的时候由于实现了Queue(队列)接口,又添加了一些新的方法:
public E peek()
获取但不移除此列表的头(第一个元素)。如果此列表为空,则返回null。
public E element()
获取但不移除此列表的头(第一个元素)。如果此列表为空,则抛出NoSuchElementException异常。
public E poll()
获取并移除此列表的头(第一个元素)。如果此列表为空,则返回null。
public E remove()
获取并移除此列表的头(第一个元素)。如果此列表为空,则抛出NoSuchElementException异常。
public boolean offer(E e)
将指定元素添加到此列表的末尾(最后一个元素)。
在JDK1.6的时候,LinkedList不直接实现Queue,而是改为实现了一个Deque(双端队列,由Deque直接继承了Queue)接口,针对链表的头,针链表的尾部,又提供了一套新方法:
public boolean offerFirst(E e)
在此列表的开头插入指定的元素。
public boolean offerLast(E e)
在此列表末尾插入指定的元素。
public E peekFirst();
获取但不移除此列表的头(第一个元素)。如果此列表为空,则返回null。
public E peekLast();
获取但不移除此列表的最后一个元素。如果此列表为空,则返回 null。
public E pollFrist()
获取并移除此列表的头(第一个元素)。如果此列表为空,则返回 null。
public E pollLast()
获取并移除此列表的最后一个元素。如果此列表为空,则返回 null。
总结: 可以看出来,不同JDK时期提供了不同的方法实现了相同的功能,为了兼容版本,并没有移除老的方法,但是JDK1.6提供的新方法,比JDK1.2和JDK1.5的方法功能强大,我们在时使用,建议使用JDK1.6提供的新的API方法。
- JDK 1.2、JDK1.5:链表当中元素的个数为null时,在使用方法的时候,可能会抛出NoSuchElementException异常。
- JDK 1.6:链表当中元素的个数为null时,在使用方法的时候,不会报异常,会返回null。
3 LinkedList的源码解析
如上图所示,LinkedList属于线性表的链式存储结构的实现(Vector和ArrayLsit都是线性表的顺序存储结构的实现),LinkedList底层使用的双向链表结构,外部保持有一个头节点和一个尾节点引用,双向链表意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。
由于设计到数据结构,因此需要具有数据结构的部分知识。本文不会讲解数据结构的知识,主要讲解源码,关于线性表的顺序结构的实现和链式结构的实现以及区别,在我的 数据结构与算法专栏 中有专门的相关文章详细介绍:Java中的线性表数据结构详解以及实现案例。实际上线性表还是很好理解的!
3.1 主要类属性
// LinkedList中元素个数
transient int size = 0;
//链表头节点
transient Node<E> first;
//链表尾节点
transient Node<E> last;
头节点和尾节点的加入,让LinkedList可以从第一个节点添加也可以从最后一个节点添加,也就是说可以作为先进先出(FIFO)的队列,也可以作为LIFO(后进先出)的栈。
3.2 Node节点
不同于ArrayList的内部数组具有的天然顺序存储,链表的实现需要记录某个元素的前驱后者后继,因此LinkedList使用一个对象Node来作为元素节点。
Node作为LinkedList的核心,也就是LinkedList中真正用来存储元素的数据结构。内部类Node就是实际的节点对象,或者说每个节点存放一个Node,用于存放实际元素的地方。
在JDK1.6及以前Node被叫做Entry。
private static class Node<E> {
/**
* 数据域,实际存放的元素,节点的值
*/
E item;
/**
* 后继,储存下一个节点的引用
*/
Node<E> next;
/**
* 前驱,储存上一个节点的引用
*/
Node<E> prev;
/**
* Node节点的构造函数
*
* @param prev 前驱,即上一个节点的引用
* @param element 存储的元素的值
* @param next 后继,即下一个节点的应用
*/
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
3.3 构造器
LinkedList有两个构造器,并且都很简单。
3.3.1 LinkedList()
构造一个空列表,里面没有任何实现,仅仅只是将header节点的前一个元素、后一个元素都指向自身(null)。
public LinkedList() {
}
3.3.2 LinkedList(Collection<? extends E> c)
构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。
public LinkedList(Collection<? extends E> c) {
//首先调用无参构造其,创建一个空列表
this();
//调用addAll方法
addAll(c);
}
3.4 添加的方法
3.4.1 添加到尾部的方法
public boolean add(E e)
将指定元素添加到此列表的结尾。
源码解析:
public boolean add(E e) {
//内部调用linkLast方法
linkLast(e);
return true;
}
/**
* 将元素链接到链表尾部
* @param e 被添加的元素
*/
void linkLast(E e) {
//创建一个节点引用l,指向原尾节点
final Node<E> l = last;
//创建新的要插入的节点作为新的尾节点,它的prev就是当前尾节点,e就是存储的数据e,next节点为null
final Node<E> newNode = new Node<>(l, e, null);
//新的要插入的节点作为新的尾节点
last = newNode;
//判断原尾节点是否为null,即原集合有没有节点数据
if (l == null)
//如果原尾节点为null,即原集合没有节点数据,那么将新节点同时也作为首节点
first = newNode;
else
//如果原尾节点不为null,即原集合有节点数据,那么将原来尾节点的next引用指向新的尾节点
l.next = newNode;
//节点数自增1
size++;
//结构改变的次数自增1
modCount++;
}
其他添加到尾部的方法,原理都是一样的:
public void addLast(E e)
public void addLast(E e) {
linkLast(e);
}
public boolean offerLast(E e)
public boolean offerLast(E e) {
addLast(e);
return true;
}
public boolean offer(E e)
public boolean offer(E e) {
return add(e);
}
3.4.2 添加到头部的方法
public void addFirst(E e)
将指定元素插入此列表的开头。
public void addFirst(E e) {
//内部调用linkFirst方法
linkFirst(e);
}
/**
* 将元素链接到链表头部
* @param e 被添加的元素
*/
private void linkFirst(E e) {
//创建一个节点引用l,指向原头节点
final Node<E> f = first;
//创建新的要插入的节点作为新的头节点,它的prev就是null,e就是存储的数据e,next节点为当前头节点
final Node<E> newNode = new Node<>(null, e, f);
//新的要插入的节点作为新的头节点
first = newNode;
//判断原头节点是否为null,即原集合有没有节点数据
if (f == null)
//如果原头节点为null,即原集合没有节点数据,那么将新节点同时也作为尾节点
last = newNode;
else
//如果原头节点不为null,即原集合有节点数据,那么将原来头节点的prev引用指向新的头节点
f.prev = newNode;
//节点数自增1
size++;
//结构改变的次数自增1
modCount++;
}
可以到,这添加到尾部节点的方法是差不多的。
其他添加到尾部的方法,原理都是一样的:
public boolean offerFirst(E e)
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
3.4.3 指定位置添加
public void add(int index,E element)
index - 要在其中插入指定元素的索引
element - 要插入的元素
在指定位置添加元素,多了一步是找到对应原索引处的元素,然后同样改变引用关系就行了,原索引的元素链接在新元素的后面。
源码解析:
add(int index,E element)
public void add(int index, E element) {
//检查索引是否处于[0-size]之间
checkPositionIndex(index);
//判断索引index是否等于size
if (index == size)
//如果索引index等于size,那实际上就是添加在链表尾部,调用linkLast(element)方法就行了
linkLast(element);
else
//如果不等,调用linkBefore方法
linkBefore(element, node(index));
}
/**
* 检查索引的方法
* @param index 索引
*/
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 索引是否大于等于0,且小于等于size
* @param index 索引
* @return 是,返回true;否,返回false
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
linkBefore方法需要给定两个参数,一个插入节点的值,一个查找到的指定索引处的node,所以我们首先需要调用node(index)方法去找到index对应的node。
/**
* 获取对应索引处的Node节点
* @param index 索引
* @return 对应的节点
*/
Node<E> node(int index) {
/*判断索引是否小于节点数量的一半,size >> 1是位运算,就是size/2的意思*/
if (index < (size >> 1)) {
/*如果索引小于节点数量的一半,那么从链表头开始查找对应索引处的节点*/
//获取头节点的引用x
Node<E> x = first;
//循环获取x的next节点,获取一次i++,当i=index时,就表示找到了对应索引处的元素
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
/*如果索引大于节点数量的一半,那么从链表尾开始查找对应索引处的节点*/
//获取尾节点的引用x
Node<E> x = last;
//循环获取x的prev节点,获取一次i--,当i=index时,就表示找到了对应索引处的元素
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
从node节点查找的实现可以看出来,这里的索引的查找,是从头或者尾开始循环遍历查找,时间复杂度为O(n),因此效率相对于ArrayLsit的O(1)的查找时间复杂度来说,效率比较低。
接下来是linkBefore方法:
/**
* 在 指定节点 前插入一个元素,这里 指定节点对象不为 null
* @param e 要存入的元素值
* @param succ 指定节点
*/
void linkBefore(E e, Node<E> succ) {
// 获取指定节点 succ 前面的一个节点
final Node<E> pred = succ.prev;
//新建一个节点,头部指向 succ 前面的节点,尾部指向 succ 节点,数据为 e
final Node<E> newNode = new Node<>(pred, e, succ);
//让 succ 节点头部指向 新建的节点
succ.prev = newNode;
//如果 succ 前面的节点为空,说明 succ 就是第一个节点,那现在新建的节点就变成第一个节点了
if (pred == null)
first = newNode;
else
//如果前面有节点,让前面的节点的下一个节点指向新节的点
pred.next = newNode;
//节点数自增1
size++;
//结构改变的次数自增1
modCount++;
}
3.5 移除的方法
3.5.1 移除头节点的方法
E remove()
获取并移除此列表的头(第一个元素)。如果列表为null,则抛出NoSuchElementException异常。
/**
* 获取并移除此列表的头(第一个元素)。如果列表为null,则抛出NoSuchElementException异常。
*
* @return 被移除的元素的值
*/
public E remove() {
//内部直接调用removeFirst方法
return removeFirst();
}
/**
* 获取并移除此列表的头(第一个元素)。如果列表为null,则抛出NoSuchElementException异常。
*
* @return 被移除的元素的值
*/
public E removeFirst() {
//获取头结点的引用
final Node<E> f = first;
//如果头节点为null,则抛出NoSuchElementException异常,说明该集合元素没有元素
if (f == null)
throw new NoSuchElementException();
//如果头节点不为null,则调用unlinkFirst方法
return unlinkFirst(f);
}
/**
* 获取并移除此列表的头(第一个元素)。
*
* @param f 头结点
* @return 头结点的值
*/
private E unlinkFirst(Node<E> f) {
//获取该节点(头节点)的值
final E element = f.item;
//获取头结点的下一个节点
final LinkedLisNode<E> next = f.next;
//该节点(头节点)的节点值置null
f.item = null;
//该节点(头节点)的下一个节点引用值置null
f.next = null;
//使头节点引用指向下一个节点
first = next;
//如果下一个节点为null,即只有一个元素,此时集合为空
if (next == null)
//那么尾节点的引用也置null
last = null;
else
//下一个节点不为null,即不只有一个元素,将下一个节点作为头节点,并将下一个节点的prev引用置null
next.prev = null;
//节点数自减1
size--;
//结构改变的次数自增1
modCount++;
//返回原头节点的值,此时没有任何引用指向原头节点,原头节点将被gc清理
return element;
}
其他移除头结点的方法,原理都是一样的:
E removeFirst()
获取并移除此列表的头(第一个元素)。如果列表为null,则抛出NoSuchElementException异常。
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
E poll()
获取并移除此列表的头(第一个元素)。如果列表为null,则返回null。
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
E pollFrist()
获取并移除此列表的头(第一个元素)。如果列表为null,则返回null。
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
3.5.2 移除尾节点的方法
public E removeLast()
获取并移除此列表的尾(最后一个元素)。如果列表为null,则抛出NoSuchElementException异常。
/**
* 获取并移除此列表的尾(最后一个元素)。如果列表为null,则抛出NoSuchElementException异常。
*
* @return 被移除的元素的值
*/
public E removeLast() {
//获取尾结点的引用
final Node<E> l = last;
//如果尾节点为null,则抛出NoSuchElementException异常,说明该集合元素没有元素
if (l == null)
throw new NoSuchElementException();
//如果尾节点不为null,则调用unlinkFirst方法
return unlinkLast(l);
}
/**
* 获取并移除此列表的尾(最后一个元素)。
*
* @return 被移除的元素的值
*/
private E unlinkLast(Node<E> l) {
//获取该节点(尾节点)的值
final E element = l.item;
//获取尾节点的上一个节点
final Node<E> prev = l.prev;
//该节点(尾节点)的节点值置null
l.item = null;
//该节点(尾节点)的下一个节点引用值置null
l.prev = null;
//使尾节点引用指向上一个节点
last = prev;
//如果上一个节点为null,即只有一个元素,此时集合为空
if (prev == null)
//那么头节点的引用也置null
first = null;
else
//上一个节点不为null,即不只有一个元素,将上一个节点作为尾节点,并将上一个节点的next引用置null
prev.next = null;
//节点数自减1
size--;
//结构改变的次数自增1
modCount++;
//返回原尾节点的值,此时没有任何引用指向原尾节点,原尾节点将被gc清理
return element;
}
其他移除尾结点的方法,原理都是一样的:
public E pollLast()
获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
3.5.2 指定位置移除
E remove(int index)
移除此列表中指定位置处的元素。返回从列表中删除的元素。
和删除头、尾节点的原理差不多,多了一步找到对应索引处的元素,然后改变前后节点的引用即可。
/**
* 移除此列表中指定位置处的元素,后面索引处的元素链接在被删除的位置上,返回从列表中删除的元素。
*
* @param index
* @return
*/
public E remove(int index) {
//检查索引,超出范围index >= 0 && index < size,将抛出IndexOutOfBoundsException
//注意这里和指定位置添加元素时检查的索引范围不一样,删除是[0,size),添加是[0,size]
checkElementIndex(index);
//调用unlink方法,传入该指定索引处的节点
return unlink(node(index));
}
/**
* 取消链接非空节点 x
*
* @param x 查找到的指定索引处的节点
* @return 删除的节点的值
*/
E unlink(Node<E> x) {
// 获取该节点的值
final E element = x.item;
//创建节点引用next,指向该节点的下一个节点
final Node<E> next = x.next;
//创建节点引用prev,指向该节点的上一个节点
final Node<E> prev = x.prev;
//判断该节点是否存在上一个节点
if (prev == null) {
//不存在上一个节点,则该节点为首节点,则让首节点引用指向该节点的下一个节点
first = next;
} else {
//存在上一个节点,则上一个节点的下一个节点引用指向该节点的下一个节点
prev.next = next;
//该节点的上一个节点的引用指向null
x.prev = null;
}
//判断该节点是否存在下一个节点
if (next == null) {
//不存在下一个节点,则该节点为尾节点.则让尾节点引用指向该节点的上一个节点
last = prev;
} else {
//存在下一个节点,则下一个节点的上一个节点引用指向该节点的上一个节点
next.prev = prev;
//该节点的下一个节点的引用指向null
x.next = null;
}
//该节点的值置空.此时没有任何引用指向该node节点对象,该对象将会被垃圾回器回收
x.item = null;
//节点数自减1
size--;
//结构改变的次数自增1
modCount++;
//返回该节点的值
return element;
}
3.6 获取的方法
3.6.1 获取头节点的方法
public E getFirst()
返回此列表的第一个元素。如果此列表为空,则抛出NoSuchElementException异常。
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E peek()
获取但不移除此列表的头(第一个元素)。如果此列表为空,则返回null。
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E element()
获取但不移除此列表的头(第一个元素)。如果此列表为空,则抛出NoSuchElementException异常。
public E element() {
return getFirst();
}
public E peekFirst();
获取但不移除此列表的第一个元素;如果此列表为空,则返回null。
返回指定索引处的Node
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
3.6.2 获取尾节点的方法
public E getLast()
返回此列表的最后一个元素。如果此列表为空,则抛出NoSuchElementException异常。
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
public E peekLast()
获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
3.6.3 指定位置获取
public E get(int index)
返回此列表中指定位置处的元素。
/**
* 返回此列表中指定位置处的元素。
* @param index 指定索引
* @return 该位置的元素的值
*/
public E get(int index) {
//检查索引,超出范围:index >= 0 && index < size,即[0,size),将抛出IndexOutOfBoundsException
checkElementIndex(index);
//返回找到的节点的值
return node(index).item;
}
可以看到内部调用的node方法顺序查找指定索引的元素,因此查找方法效率不是很高。
3.7 contains和indexOf
public boolean contains(Object o)
如果此列表包含指定元素,则返回 true。方法通过判断indexOf(Object o)方法返回的值是否是-1来判断链表中是否包含对象o。
public int indexOf(Object o)
返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。方法内部是通过equals方法来判断两个元素值是否相等的。因此一般来说如果存入数据是对象,那么需要重写equals方法。
/**
* 如果此列表包含指定元素,则返回 true。
*
* @param o 比较的元素
* @return true 存在 false 不存在
*/
public boolean contains(Object o) {
//内部直接调用indexOf的方法来判断的
return indexOf(o) != -1;
}
/**
* 返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。
*
* @param o 查找的元素
* @return 出现的索引,-1表示不存在
*/
public int indexOf(Object o) {
int index = 0;
// 从前向后顺序遍历查找链表,返回"值为对象(o)的节点对应的索引" 不存在就返回-1
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
//可以看到LinkedList是通过元素值的equals方法来比较是否相等的
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
3.8 clone方法
和大多数集合类一样,LinkedList的克隆方法并不会对数据进行克隆,算是浅克隆。
/**
* 浅克隆(元素不会被克隆)
* @return 一个副本
*/
public Object clone() {
//调用父类(Object)的克隆方法
LinkedList<E> clone = superClone();
//清空克隆对象的内部变量引用值
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
// 重新赋值
// 深克隆Node,这时新旧对象的Node是不一样了,但是对应索引的Node中的Item却还是浅克隆,指向了同一个对象,改变对象的值会对两个链表都产生影响!
// 但是如果储存的是直接量则不会影响!
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
//返回被克隆对象
return clone;
}
@SuppressWarnings("unchecked")
private LinkedList<E> superClone() {
try {
//Object的clone方法
return (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
3.9 clear方法
清空链表,并不是简单的将外部引用置空,而是循环整个链表,将所有节点之间的关联一一解除。
/**
* 清空链表
*/
public void clear() {
//手动清理全部链表节点之间的引用关系,帮助GC
for (LinkedList.Node<E> x = first; x != null; ) {
LinkedList.Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
//最后将外部的头\尾节点引用置空
first = last = null;
//size置0
size = 0;
//结构改变的次数自增1
modCount++;
}
4 迭代器机制
凡是List体系的集合都具有Iterator 和ListIterator 迭代器,他们的方法都是一样的,只是具体的实现是由这些实现类自己来实现的!
Iterator 和ListIterator 迭代器的方法和ArrayList的迭代器的实现,包括快速失败和安全失败以及“并发修改”异常机制等都在这篇文章:Java的ArrayList集合源码深度解析以及应用介绍中有讲解,下面遇到相同原理的时候不会赘述!
下面来看看LinkedList集合的迭代器的实现,直觉告诉我们,这两个集合的实现是有区别的!
4.1 Iterator迭代器
List体系的集合的迭代器提供的方法都是一样的,只是提供了不同的实现,下面是LinkedList的Iterator迭代器的一个正向遍历-删除使用案例。
所谓的正向遍历就是从头到尾,那么反向遍历就是从尾到头了,不过反向遍历是ListIterator迭代器的功能,后面会讲!
@Test
public void test2() {
LinkedList<Integer> linkedList = new LinkedList<>(Arrays.asList(1, 2, 3));
System.out.println("移除元素前: " + linkedList);
//获取Iterator迭代器实例
Iterator<Integer> iterator = linkedList.iterator();
//循环处理数据
while (iterator.hasNext()) {
//获取下一个元素
Integer next = iterator.next();
System.out.println(next);
//移除下一个元素
iterator.remove();
}
System.out.println("移除元素后: " + linkedList);
}
4.1.1 Iterator源码介绍
下面来看Iterator迭代器在LinkedList中的实现!
首先我们跟进iterator(),该方法用于获取迭代器,但是我们在LinkedList的实现类中并没有找到iterator()方法,此时我们知道,那肯定调用的方法在父类中被定义了,并且子类没有重写该方法。
然后进入LinkedList的父类AbstractSequentialList,在父类中,我们果然找到了iterator()方法的源码:
/**
* 父类AbstractSequentialList中的iterator()方法
* @return 返回迭代器实例
*/
public Iterator<E> iterator() {
//竟然是返回的一个listIterator对象实例
return listIterator();
}
我们能够看到,所调用iterator()方法,竟然是返回的一个listIterator的实例(调用的listIterator()方法),但这是可以成立的,因为ListIterator接口继承了Iterator接口,Java中返回的子类对象可以使用父类来接收!
接下来进入listIterator()方法,我们发现竟然来到了AbstractList类中,原来,LinkedList的父类AbstractSequentialList也没有重写listIterator()方法,该方法出现在AbstractSequentialList的父类——AbstractList类中,竟然这么复杂,但是我们还是接着看吧,我们查看AbstractList类的listIterator()实现:
/**
* 父类AbstractSequentialList的父类AbstractList中的listIterator()方法
* @return 返回一个列表迭代器
*/
public ListIterator<E> listIterator() {
//内部调用listIterator(0)方法,接受一个参数0
return listIterator(0);
}
内部又调用了另外一个带参的listIterator(0)方法,那么这个方法又在哪里呢?我们发现LinkedList已经重写了这个带参数的方法,那么这一次肯定是调用的自己的重写的方法了,看看它的实现:
/**
* LinkedList重写的带参数的方法
* @param index 实际上表示从列表迭代器返回的第一个元素的索引,即开始迭代的元素的索引
* @return 一个列表迭代器实例
*/
public ListIterator<E> listIterator(int index) {
/*检查索引是否越界*/
checkPositionIndex(index);
/*返回ListItr实例,该实例就是一个Iterator对象*/
return new ListItr(index);
}
/**
* 检查索引是否越界
* @param index
*/
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 检查索引是否大于等于0并且小于等于size
* @param index 索引
* @return true-是 false-否
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
在进行索引范围检查之后,终于我们看到了返回的实例,一个ListItr的对象。然后我们在LinkedList类中找到了ListItr内部类的实现,我们来看看比较常用的方法:
/**
* 内部类ListItr实现了ListIterator接口,因此除了具有Iterator接口的方法之外还具有更多的方法。
* 例如add(),set()这里先不介绍,先来看看Iterator接口的正向迭代方法
*/
private class ListItr implements ListIterator<E> {
/**
* 最后被返回的节点
*/
private Node<E> lastReturned;
/**
* 下一个被返回的节点
*/
private Node<E> next;
/**
* 下一个被返回的节点的索引
*/
private int nextIndex;
/**
* 预期结构修改计数 expectedModCount = modCount,用于检测"并发修改"异常
*/
private int expectedModCount = modCount;
/**
* 构造器
* @param index 表示从列表迭代器返回的第一个元素的索引,即开始迭代的元素的索引
*/
ListItr(int index) {
//获取下一个要被返回的节点,如果index == size 那说明下一个将要被返回的节点是null(因为索引是从0开始计数的);
//否则便是使用node(index)方法查找下一个被返回的节点
next = (index == size) ? null : node(index);
//获取下一个要被返回的节点的索引,让其等于index
nextIndex = index;
}
/**
* 是否具有下一个节点
* @return true-有 false-无
*/
public boolean hasNext() {
//如果下一个要被返回的节点的索引小于size,那么就返回true,表示还存在没有迭代的节点
// 否则返回false,表示已经迭代完毕
return nextIndex < size;
}
/**
* 获取下一个节点
* @return 下一个节点的值
*/
public E next() {
/*检测"并发修改"*/
checkForComodification();
/*检测是否还存在可以获取的节点*/
if (!hasNext())
throw new NoSuchElementException();
//设置最后被返回的节点指向next节点
lastReturned = next;
//next节点指向next节点的下一个节点
next = next.next;
//下一个被返回的节点的索引自增1
nextIndex++;
//返回最后被返回的节点的值
return lastReturned.item;
}
/**
* 移除下一个节点
*/
public void remove() {
/*检测"并发修改"*/
checkForComodification();
/*检测最后被返回的节点是否为null,当创建了迭代器对象直接使用该方法时,lastReturned就指向null
* 因此不能先于next()方法使用,也不能连续使用remove()方法*/
if (lastReturned == null)
throw new IllegalStateException();
/*获取最后被返回的节点的下一个节点*/
Node<E> lastNext = lastReturned.next;
/*移除lastReturned节点和链表之间的引用关系,后续的节点链接到原节点的位置*/
unlink(lastReturned);
/*如果下一个节点等于最后被返回的节点*/
if (next == lastReturned)
//那么下一个节点的引用指向最后被返回的节点的下一个节点,实际上这是为倒序迭代遍历服务的(previous),正序遍历使用不到
next = lastNext;
else
//否则下一个被返回的节点的索引自减1,实际上这是为正序迭代遍历服务的(next),倒序遍历使用不到
nextIndex--;
//将最后被返回的节点的引用指向null
lastReturned = null;
//由于调用了外部类的unlink方法,因此modCount肯定自增了1,为了保证不出现"ConcurrentModificationException"异常,最后将expectedModCount也自增1
expectedModCount++;
}
/**
* 检测"并发修改",如果出现则抛出ConcurrentModificationException异常
*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
4.1.2 Iterator实例解析
下面结合源码以及上面的使用案例来介绍LinkedList中的Iterator迭代器的正向遍历-删除是如何工作的。
首先创建了一个链表,它的元素节点是1——2——3。
然后创建一个Iterator迭代器实例,实际上是返回的ListItr对象,该对象直接实现了ListIterator迭代器,但是由于ListIterator迭代器继承了Iterator迭代器,由于Java的向上转型,因此该返回是合理的,只是我们只能使用Iterator迭代器中的方法而已。
根据源码,new ListItr(index)中index默认传入的是0,那么在ListItr对象的构造器中,next=1节点,nextIndex=0,此时lastReturned=null,expectedModCount被初始化等于modCount。
然后开始第一次循环,首先是hasNext()方法,明显nextIndex=0<size=3是成立的,因此进入循环体中。
接下来是next()方法,该方法获取下一个元素,首先是一系列“并发修改”检查,关于并发修改检查(快速失败or安全失败),在ArrayList部分有详细讲解,这里不多赘述。明显这里是可以通过检查的。然后lastReturned = next=1节点,next= next.next=2节点,nextIndex++之后变成1。然后返回lastReturned的值,即返回1。
这里打断一下,如果后续没有remove方法,即循环中只有next()遍历的方法,此时进入第二轮循环,nextIndex=1<size=3,然后继续执行next()方法,这一次lastReturned = next=2节点,next= next.next=3节点,nextIndex++之后变成2。然后返回lastReturned的值,即返回2。然后继续循环,直到3节点遍历完毕,此时nextIndex=3,不满足小于size的条件,此时循环迭代遍历结束!因此正向循环遍历的原理还是很简单,他维护了一个nextIndex遍历一次就自增一次,当等于size时,表示集合迭代遍历完毕!
现在回来,我们接着案例讲,接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的next方法中lastReturned=1节点,因此不为null。然后获取lastNext=lastReturned.next=2节点,然后调用unlink方法解除lastReturned指向的1节点与链表的关系,此时链表节点变成了2——3。然后判断next是否等于lastReturned,这里说明一下,如果是倒序遍历,那么就会返回true,正序遍历就会返回false,我们本次的遍历明显是正序遍历,因此肯定返回false,然后将nextIndex–之后又变成了0,最后expectedModCount++,因为unlink方法中modCount会自增1,为了防止出现ConcurrentModificationException异常,因此这里expectedModCount也而要自增1。
到此第一轮循环完毕,循环完毕之后我们看到next返回1,即0索引处的1节点的值,remove则是把1节点移除链表,最后nextIndex=0,next=2节点,lastReturned=null,expectedModCount还是等于modCount。
然后开始第二轮循环,首先是hasNext()方法,注意,由于上一轮循环中的unlink操作,会导致size减去1,这里size=2,但是明显nextIndex=0<size=2还是成立的,因此进入循环体中。
接下来是next()方法,该方法获取下一个元素,首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后lastReturned = next=2节点,next= next.next=3节点,nextIndex++之后变成1。然后返回lastReturned的值,即返回2。
接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的next方法中lastReturned=2节点,因此不为null。然后获取lastNext=lastReturned.next=3节点,然后调用unlink方法解除lastReturned指向的2节点与链表的关系,此时链表节点变成了3。然后判断next是否等于lastReturned,返回false,然后将nextIndex–之后又变成了0,最后expectedModCount++,让expectedModCount和modCount始终相等。
到此第二轮循环完毕,循环完毕之后我们看到next返回2,即0索引处的2节点的值,remove则是把2节点移除链表,最后nextIndex=0,next=3节点,lastReturned=null,expectedModCount还是等于modCount。
然后开始第三轮循环,首先是hasNext()方法,这里size=1,但是明显nextIndex=0<size=1还是成立的,因此进入循环体中。
接下来是next()方法,该方法获取下一个元素,首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后lastReturned = next=3节点,next= next.next=null,nextIndex++之后变成1。然后返回lastReturned的值,即返回3。
接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的next方法中lastReturned=3节点,因此不为null。然后获取lastNext=lastReturned.next=null,然后调用unlink方法解除lastReturned指向的3节点与链表的关系,此时链表没有了节点。然后判断next是否等于lastReturned,返回false,然后将nextIndex–之后又变成了0,最后expectedModCount++,让expectedModCount和modCount始终相等。
到此第三轮循环完毕,循环完毕之后我们看到next返回3,即0索引处的3节点的值,remove则是把3节点移除链表,最后nextIndex=0,next=null,lastReturned=null,expectedModCount还是等于modCount。
然后开始第四轮循环,首先是hasNext()方法,这里size=0,明显nextIndex=0<size=0不成立了,因此循环结束,此时链表节点已经被遍历、删除完毕,再次打印链表,只会返回“[]”。
总结:
我们看到,LinkedList的Iterator迭代器的正序遍历-删除思想和ArrayList的迭代器的遍历-删除思想是一致的,即始终定位到链表头部,遍历一个删除一个,后续的节点自动成为头节点,而循环的条件始终是nextIndex=0和size的比较,删除一次size减少1,当size等于0时,自然循环结束,遍历-删除的操作结束,可以看到它的原理还是比较简单的!
4.2 ListIterator迭代器
ListIterator继承了Iterator迭代器,功能更加强大!可以实现反向遍历,add、set等操作。下面是LinkedList的ListIterator迭代器的一个反向遍历-删除使用案例。
/**
* ListIterator迭代器的反向遍历+删除案例
*/
@Test
public void test3() {
LinkedList<Integer> linkedList = new LinkedList<>(Arrays.asList(1, 2, 3));
System.out.println("移除元素前: " + linkedList);
//获取ListIterator迭代器实例
ListIterator<Integer> iterator = linkedList.listIterator(3);
//反向遍历 处理数据
while (iterator.hasPrevious()) {
//获取下一个元素
Integer next = iterator.previous();
System.out.println(next);
//移除下一个元素
iterator.remove();
}
System.out.println("移除元素后: " + linkedList);
}
4.2.1 ListIterator源码介绍
下面来看ListIterator迭代器在LinkedList中的实现!
首先我们跟进listIterator(),该方法用于获取迭代器,但是我们在LinkedList的实现类中并没有找到listIterator()方法,此时我们知道,那肯定调用的方法在父类中被定义了,并且子类没有重写该方法。
然后在LinkedList的父类AbstractSequentialList中,我们也没有找到listIterator()方法,继续向上查找,在祖父类AbstractList中,我们果然找到了listIterator()方法的源码:
public ListIterator<E> listIterator() {
return listIterator(0);
}
是不是觉得似曾相似,没错,上面讲的iterator()方法也是间接的调用了这个方法,没想到吧!
实际上,LinkedList的iterator()和listIterator()返回的都是ListItr内部类对象,不同之处只是前面的静态类型不一样,简化来写就是这样的:
Iterator iterator=new ListItr(0);
ListIterator listIterator=new ListItr(0);
那这么说来上面的iterator实际上已经拥有了原本属于listIterator的方法?
在我们看来的确是这样的,但是根据Java方法的调用规则,只有左边静态类型拥有的方法才能被调用,上面的iterator虽然实际类型是ListItr,但是由于静态类型属于Iterator类型,因此还是只能调用Iterator迭代器的方法,而下面的listIterator由于是ListIterator类型,则可以调用全部的方法!
方法调用这里涉及到了JVM层面,不了解Java方法调用规则的可以看看这篇文章:Java的JVM运行时栈结构和方法调用详解。
由于ListIterator迭代器增加了很多方法,我就以上面的反向遍历-删除为例子来讲解关键方法的源码:
/**
* 内部类ListItr实现了ListIterator接口,因此除了具有Iterator接口的方法之外还具有更多的方法。
* 例如add(),set()这里先不介绍,先来看看Iterator接口的正向迭代方法
*/
private class ListItr implements ListIterator<E> {
/**
* 最后被返回的节点
*/
private Node<E> lastReturned;
/**
* 下一个被返回的节点
*/
private Node<E> next;
/**
* 下一个被返回的节点的索引
*/
private int nextIndex;
/**
* 预期结构修改计数 expectedModCount = modCount,用于检测"并发修改"异常
*/
private int expectedModCount = modCount;
/**
* 构造器
* @param index 表示从列表迭代器返回的第一个元素的索引,即开始迭代的元素的索引
*/
ListItr(int index) {
//获取下一个要被返回的节点,如果index == size 那说明下一个将要被返回的节点是null(因为索引是从0开始计数的);
//否则便是使用node(index)方法查找下一个被返回的节点
next = (index == size) ? null : node(index);
//获取下一个要被返回的节点的索引,让其等于index
nextIndex = index;
}
/**
* 是否具有下一个节点
* @return true-有 false-无
*/
public boolean hasNext() {
//如果下一个要被返回的节点的索引小于size,那么就返回true,表示还存在没有迭代的节点
// 否则返回false,表示已经迭代完毕
return nextIndex < size;
}
/**
* 获取下一个节点
* @return 下一个节点的值
*/
public E next() {
/*检测"并发修改"*/
checkForComodification();
/*检测是否还存在可以获取的节点*/
if (!hasNext())
throw new NoSuchElementException();
//设置最后被返回的节点指向next节点
lastReturned = next;
//next节点指向next节点的下一个节点
next = next.next;
//下一个被返回的节点的索引自增1
nextIndex++;
//返回最后被返回的节点的值
return lastReturned.item;
}
/**
* 移除下一个节点
*/
public void remove() {
/*检测"并发修改"*/
checkForComodification();
/*检测最后被返回的节点是否为null,当创建了迭代器对象直接使用该方法时,lastReturned就指向null
* 因此不能先于next()方法使用,也不能连续使用remove()方法*/
if (lastReturned == null)
throw new IllegalStateException();
/*获取最后被返回的节点的下一个节点*/
Node<E> lastNext = lastReturned.next;
/*移除lastReturned节点和链表之间的引用关系,后续的节点链接到原节点的位置*/
unlink(lastReturned);
/*如果下一个节点等于最后被返回的节点*/
if (next == lastReturned)
//那么下一个节点的引用指向最后被返回的节点的下一个节点,实际上这是为倒序迭代遍历服务的(previous),正序遍历使用不到
next = lastNext;
else
//否则下一个被返回的节点的索引自减1,实际上这是为正序迭代遍历服务的(next),倒序遍历使用不到
nextIndex--;
//将最后被返回的节点的引用指向null
lastReturned = null;
//由于调用了外部类的unlink方法,因此modCount肯定自增了1,为了保证不出现"ConcurrentModificationException"异常,最后将expectedModCount也自增1
expectedModCount++;
}
/**
* 检测"并发修改",如果出现则抛出ConcurrentModificationException异常
*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
4.2.2 ListIterator实例解析
下面结合源码以及上面的使用案例来介绍LinkedList中的ListItertor迭代器的反向遍历-删除是如何工作的。
首先创建了一个链表,它的元素节点是1——2——3。
然后创建一个ListItertor迭代器实例,实际上是返回的ListItr对象。根据源码,new ListItr(index)中index在本案例中指定传入的是3,那么在ListItr对象的构造器中,由于index和size都等于3,因此next=null,nextIndex=3,此时lastReturned=null,expectedModCount被初始化等于modCount。
然后开始循环,首先是hasPrevious ()方法,明显nextIndex=3>0是成立的,因此进入循环体中。
接下来是previous()方法,该方法获取上一个元素(因为是倒序遍历,即重尾到头),首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后由于next等于null,那么设置lastReturned = next = last节点,这里的last节点就是3节点。nextIndex–之后变成2。然后返回lastReturned的值,即返回3。
这里打断一下,如果后续没有remove方法,即循环中只有previous()遍历的方法,此时进入第二轮循环,nextIndex=2>0,然后继续执行previous()方法,这一次由于next不等于nulll,那么设置lastReturned = next = next.prev节点,这里的next.prev节点就是2节点。nextIndex–之后变成1。然后返回lastReturned的值,即返回2。然后继续循环,直到1节点遍历完毕,此时nextIndex=0,不满足大于0的条件,此时循环迭代遍历结束!因此反向循环遍历的原理也还是很简单,它维护了一个nextIndex遍历一次就自减一次,当等于0时,表示集合反向迭代遍历完毕!
现在回来,我们接着案例讲,接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的previous方法中lastReturned=3节点,因此不为null。然后获取lastNext=lastReturned.next=null,然后调用unlink方法解除lastReturned指向的3节点与链表的关系,此时链表节点变成了1——2。然后判断next是否等于lastReturned,这里说明一下,如果是倒序遍历,那么就会返回true,正序遍历就会返回false,我们本次的遍历明显是倒序遍历,因此肯定返回true,然后next = lastNext=null,然后lastReturned = null,最后expectedModCount++,因为unlink方法中modCount会自增1,为了防止出现ConcurrentModificationException异常,因此这里expectedModCount也而要自增1。
到此第一轮循环完毕,循环完毕之后我们看到next返回3,即2索引处的3节点的值,remove则是把3节点移除链表,最后nextIndex=2,next=null,lastReturned=null,expectedModCount还是等于modCount。
然后开始第二轮循环,首先是hasNext()方法,显nextIndex=2>0还是成立的,因此进入循环体中。
接下来是previous()方法,该方法获取上一个元素(因为是倒序遍历,即重尾到头),首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后由于next等于null,那么设置lastReturned = next = last节点,由于last节点3被删除了,这里的last节点就是2节点。nextIndex–之后变成1。然后返回lastReturned的值,即返回2。
接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的previous方法中lastReturned=2节点,因此不为null。然后获取lastNext=lastReturned.next=null,然后调用unlink方法解除lastReturned指向的2节点与链表的关系,此时链表节点变成了1。然后判断next是否等于lastReturned,肯定返回true,然后next = lastNext=null,然后lastReturned = null,最后expectedModCount++,让expectedModCount和modCount始终相等。
到此第二轮循环完毕,循环完毕之后我们看到next返回2,即1索引处的2节点的值,remove则是把2节点移除链表,最后nextIndex=1,next=null,lastReturned=null,expectedModCount还是等于modCount。
然后开始第三轮循环,首先是hasNext()方法,显nextIndex=1>0还是成立的,因此进入循环体中。
接下来是previous()方法,该方法获取上一个元素(因为是倒序遍历,即重尾到头),首先是一系列“并发修改”检查,明显这里是可以通过检查的。然后由于next等于null,那么设置lastReturned = next = last节点,由于last节点2被删除了,这里的last节点就是1节点。nextIndex–之后变成0。然后返回lastReturned的值,即返回1。
接下来是remove方法,首先是一系列“并发修改”检查,可以通过。然后判断lastReturned是否等于null,由于上面的previous方法中lastReturned=1节点,因此不为null。然后获取lastNext=lastReturned.next=null,然后调用unlink方法解除lastReturned指向的1节点与链表的关系,此时链表没有了节点。然后判断next是否等于lastReturned,肯定返回true,然后next = lastNext=null,然后lastReturned = null,最后expectedModCount++,让expectedModCount和modCount始终相等。
到此第三轮循环完毕,循环完毕之后我们看到next返回1,即0索引处的1节点的值,remove则是把1节点移除链表,最后nextIndex=0,next=null,lastReturned=null,expectedModCount还是等于modCount。
然后开始第四轮循环,首先是hasNext()方法,这里nextIndex=0,明显nextIndex=0>0不成立了,因此循环结束,此时链表节点已经被倒序遍历、删除完毕,再次打印链表,只会返回“[]”。
总结:
我们看到,LinkedList的ListIterator迭代器的反序遍历-删除思想和ArrayList的迭代器的遍历-删除思想是一致的,即始终定位到链表头部,遍历一个删除一个,后续的节点自动成为头节点,而循环的条件始终是nextIndex=0和size的比较,删除一次size减少1,当size等于0时,自然循环结束,遍历-删除的操作结束,可以看到它的原理还是比较简单的!
4 栈和队列的模拟
栈特点:FILO 先进后出
队列的特点:FIFO先进先出
模拟队列和栈: ( 装饰设计模式 )
- 创建一个类: MyQueue or MyStack
- 引入LinkedList 类,定义为全局变量。
- 提供一个该类的构造器,对全局变量进行初始化。
- 提供存和取的方法,以及判断是否为null的方法。
4.1 模拟栈
/**
* LinkedList模拟栈
* @author lx
*/
public class MyStackFromLinkedList<T> {
private LinkedList<T> ll;
public MyStackFromLinkedList() {
this.ll = new LinkedList<>();
}
/**
* 入栈
* @param obj 入栈的元素
*/
public void add(T obj) {
ll.offerFirst(obj);
}
/**
* 出栈
* @return 出栈的元素
*/
public T get() {
return ll.pollFirst();
}
/**
* 栈是否为空
* @return true-空;false-非空
*/
public Boolean isEmpty() {
return ll.isEmpty();
}
}
/**
* 测试
*/
class MyStackTest {
public static void main(String[] args) {
MyStackFromLinkedList<String> myStack = new MyStackFromLinkedList<>();
//入栈
myStack.add("aa");
myStack.add("bb");
myStack.add("cc");
myStack.add("dd");
while (!myStack.isEmpty()) {
//出栈,先入栈的后出栈
Object o = myStack.get();
//dd cc bb aa
System.out.print(o + " ");
}
}
}
4.2 模拟队列
/**
* LinkedList模拟队列
* @author lx
*/
public class MyQueueFromLinkedList<T> {
private LinkedList<T> ll;
public MyQueueFromLinkedList() {
this.ll = new LinkedList<>();
}
/**
* 入队列
*
* @param obj 入队的元素
*/
public void add(T obj) {
ll.offerFirst(obj);
}
/**
* 出队列
*
* @return 出队的元素
*/
public T get() {
return ll.pollLast();
}
public Boolean isEmpty() {
return ll.isEmpty();
}
}
/**
* 测试
*/
class MyQueueTest {
public static void main(String[] args) {
MyQueueFromLinkedList<String> myQueue = new MyQueueFromLinkedList<>();
//入队列
myQueue.add("a");
myQueue.add("b");
myQueue.add("c");
myQueue.add("d");
while (!myQueue.isEmpty()) {
//出队列,先入队的先出队
Object o = myQueue.get();
//a b c d
System.out.print(o + " ");
}
}
}
5 数组和链表的结构的异同
相同点:
数组和链表都属于线性表,其中数组是属于顺序储存的实现,逻辑存储和物理存储相同而链表则属于链式储存的实现,逻辑存储和物理存储不相同。两种结构均实现数据结构中的逻辑顺序存储。
不同点:
数组:
- 在内存中是一组连续的内存单元。内存空间要求高,必须有足够的连续内存空间。
- 每个元素都有自己的下标索引,元素可以重复,都个元素都可以通过下标索引访问。
- 它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据量比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。
- 数组的查询:指针从开始索引按顺序前移,直到查询到对应的数据。速度快,支持随机访问。
- 增加和删除:在某个位置增加或删除元素后,其后面的元素都要重新分配下标索引,速度相对较慢(但可以接受)。
链表:
- 在内存中的节点分布是不连续的,随机的,离散的。节点之间通过保存上\下一个节点的引用来建立联系。内存利用率高,不会浪费内存。链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,因此不存在容量一说!
- 每个元素都有自己的下标索引,元素可以重复,每个元素都可以通过下标索引访问。但是和数组的底层却是不同的。
- 链表的查询:没有数组速度快,不支持随机访问,只能顺序访问;
- 增加和删除:直接操作相邻的两个节点的引用,不需要整体移动后半部分的数据,速度快。
6 ArrayList和LinkList的异同
相同点:
- 都是非线程安全的,只有在单线程下才可以使用。为了防止非同步访问,可以采用如下方式创建List list= Collections.synchronizedList(List list);
- LinkedList和ArrayList元素都可以为null,元素都有序(存储顺序)。
不同点:
- 查询:ArrayList继承于AbstractList,LinkedList继承于AbstractSequentialList;AbstractList支持快速随机访问,即支持直接通过索引查找集合元素;AbstractSequentialList不支持快速随机访问,只有通过【index < (size >> 1)】判断索引是否更靠近链表头部或者尾部,让案后选择从头或者尾开始依次遍历!即查询ArrayList速度快,LinkedList速度慢!
- 插入、删除元素:LinkedList的删除和新增方法的实现基本是对该节点的上一个节点和下一个节点的引用设置,不需要操作其他节点,相比于ArrayList来说,效率是非常高的,因为ArrayList的新增和操作需要对数组中的数据做遍历复制操作,并需要调整操作索引后的所有的数的位置。
- LinkedList没有实现自己的 Iterator(其iterator方法,底层调用的是listIterator方法),但是有 ListIterator和 DescendingIterator(实现了Iterator);
- LinkedList需要更多的内存,因为 ArrayList的每个索引的位置是实际的数据,而 LinkedList中的每个节点中存储的是实际的数据和前后节点的位置;
- ArrayList的空间浪费主要体现在 在list列表的结尾预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗相当的空间
应用:
- 数组(ArrayLsit)应用场景:频繁的查询操作,较少的增删操作。
- 链表(LinkedList)应用场景:较少的查询操作,频繁的增删操作。
7 总结
LinkedList是基于链表的线性表Java实现,它的原理还是比较简单的,实际上线性表的原理都不怎么难。
LinkedList是基于链表实现的双端队列,在JDK1.6的时候,添加了集合类ArrayDeque,该类同样实现了Deque接口,即也作为一个“双端队列”,可以被用来实现Stack(栈)或者Queue(队列)。
ArrayDeque内部是采用可变数组实现的双端队列,采用两个外部引用来保持队列头结点和尾节点的访问,同时删除队列头尾元素时不会移动其他元素,而是移动引用的位置,即形成一个环形队列,能够复用数组空间(允许队头索引比队尾索引值更大)。相比于使用链表实现的双端队列LinkedList综合效率更好!同时如果用于实现栈,那么相比于Stack的综合效率同样更好!但是ArrayDeque不支持null元素!
我们后续将会介绍的更多集合,比如Stack、TreeMap、HashMap,LinkedHashMap、HashSet、TreeSet等基本集合以及JUC包中的高级并发集合。如果想学习集合源码的关注我的专栏更新!
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!