LinkedList源码学习
LinkedList是什么
ArrayList
类实现了可变的数组,保存所有元素,可根据索引位置对集合快速随机访问;但向指定的索引位置增删对象的速度较慢。LinkedList
类采用双向链表结构保存对象,允许null存在。每个元素分配的空间不必连续,向集合中增删对象效率高;但随机访问速度较慢。
1 LinkedList的继承关系
LinkedList 继承自 AbstractSequentialList 接口,同时了还实现了 Deque, Queue 接口,可以用来模拟堆栈,队列,双端队列结构。
2 数据结构 成员变量
源码:
// 内部类
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;
}
}
// 成员变量
//链表长度
transient int size = 0;
transient Node<E> first; //指向第一个节点的指针。
transient Node<E> last; //指向最后一个节点的指针。
public LinkedList(Collection<? extends E> c) { //构造方法
this();
addAll(c); //添加元素
}
- 使用双向链表实现.
- 链表的每个节点使用内部类Node表示.
- 通过first和last分别指向链表的第一个和最后一个元素.
3 构造方法
链表结构,初始化不需要指定长度。
/**
* 构造一个空列表。
*/
public LinkedList() {
}
/**
* 构造一个包含指定集合的元素的列表,其顺序由集合的迭代器返回。
* @param c 将其元素放入此列表的集合
* @throws NullPointerException 如果指定的集合为null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
4 特点
- 元素可以为 null
- 实现了Queue、Deque接口,可以用来模拟堆栈,队列,双端队列结构
- 可以包含重复的元素
- 非线程安全
- 维护了元素插入时的顺序
- 适合插入和删除操作 只用改变一下前后节点引用
常用方法
1 增
offer()
、add()
、peek()
public boolean offer(E e) {
return add(e);
}
public boolean add(E e) {
linkLast(e);
return true;
}
offer()
、add()
底层逻辑一致
1.1 追加尾部
add()
、addLatst()
方法从尾部开始追加
public boolean add(E e) {
linkLast(e); // 将元素添加到链表末尾
return true;
}
//将e链接为最后一个元素
void linkLast(E e) {
final Node<E> l = last; // last 指向最后一个节点,在没有添加元素时为 null
final Node<E> newNode = new Node<>(l, e, null); // 创建一个新节点,l是前节点,e是当前节点的值,后节点是null
last = newNode; // 新建的节点放在尾部
if (l == null)
first = newNode; // 如果链表为空,头部和尾部是同一个节点,都是新建的节点
if (l == null)
else
l.next = newNode; // 如果链表不为空,将旧的尾节点的下一节点指向新添加的节点
size++; //大小更改
modCount++; //版本更改
}
1.2 追加头部
addFirst()
方法从头部开始追加
public void addFirst(E e) {
linkFirst(e);
}
// 从头部追加
private void linkFirst(E e) {
final Node<E> f = first; //指向头节点,赋值给f
final Node<E> newNode = new Node<>(null, e, f); // 创建一个新节点,null是前节点,e是当前节点的值,后节点是f
first = newNode; // 新建的节点放在头部
if (f == null)
last = newNode; // 如果链表为空,头部和尾部是同一个节点,都是新建的节点
else
f.prev = newNode;
size++; //大小更改
modCount++; //版本更改
}
1.3 指定位置插入
add(int index, E element)
、addAll(int index, Collection<? extends E> c)
方法从头部开始追加
public void add(int index, E element) {
checkPositionIndex(index); // 检查插入位置的索引的合理性
if (index == size)
linkLast(element); // 插入的情况是尾部插入的情况:调用linkLast()
else
//在node(index)节点之前插入
linkBefore(element, node(index)); // 插入的情况是非尾部插入的情况(中间插入):inkBefore()
}
// 检查插入位置的索引的合理性
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); //创建一个新节点,pred是前节点,e是当前节点的值,后节点是succ
succ.prev = newNode; // 更新插入位置(succ)的前置节点为新节点
if (pred == null)
first = newNode; // 如果pred为null,说明该节点插入在头节点之前,要重置first头节点
else
pred.next = newNode; // 如果pred不为null,那么直接将pred的后继指针指向newNode即可
size++; //大小更改
modCount++; //版本更改
}
addAll(int index, Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); // 1.添加位置的下标的合理性检查
Object[] a = c.toArray(); // 2.将集合转换为Object[]数组对象
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ; // 3.得到插入位置的前继节点和后继节点
if (index == size) { // 特殊情况:当索引index=size时,通过node(index)找不到节点,超过范围
// 从尾部添加的情况:前继节点是原来的last节点;后继节点是null
succ = null;
pred = last;
} else {
// 从指定位置(非尾部)添加的情况:前继节点就是index位置的节点,后继节点是index位置的节点的前一个节点
succ = node(index); //找到第index个节点
pred = succ.prev;
}
for (Object o : a) { // 4.遍历数据,将数据插入
@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 = pred; // 如果是从尾部开始插入的,则把last置为最后一个插入的元素
} else {
pred.next = succ; // 如果不是从尾部插入的,则把尾部的数据和之前的节点连起来
succ.prev = pred;
}
size += numNew; // 链表大小+num
modCount++; // 修改次数加1
return true;
}
2 删
remove()
、poll()
// remove()
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
// poll(e)
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f); // 链表为空时,poll返回null
}
几种删除方法底层逻辑一致,都是通过unlinkFirst()
实现的,区别在于空链表时,remove()
抛出异常,poll()
返回null值。
2.1 删除尾部
removeLast()
、pollLast()
方法从尾部开始删除
public E removeLast() { //从此列表中删除并返回最后一个元素。
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkLast(Node<E> l) { // 取消链接非空的最后一个节点l。
// assert l == last && l != null;
final E element = l.item; // 得到指定节点的值
final Node<E> prev = l.prev; // 得到指定节点的前继节点
l.item = null; // 删除指定节点内容
l.prev = null; // 删除指定节点的前继节点,置为null
last = prev; // 链表的尾节点设置为指定节点的前继节点
if (prev == null) // 如果prev为null,则表示删除的是头部节点,否则就不是头部节点
first = null; // 链表头部节点置空
else
prev.next = null; // 否则,指定节点的前继节点指向空
size--; // 数量减1
modCount++;
return element;
}
2.2 删除头部
remove()
、removeFirst()
、poll()
、pollFirst()
方法从头部开始删除
public E removeFirst() { //从此列表中删除并返回第一个元素。
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) { // 取消链接非空的第一个节点f。
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
2.3 删除指定元素
remove(int index)
、remove(Object o)
、removeFirstOccurrence(Object o)
、removeLastOccurrence(Object o)
方法删除指定元素,通过unlink()
方法完成删除操作。
unlink()
与unlinkFirst()
类似,区别在于unlink()
可以删除中间节点,unlinkFirst()
只删除头节点。
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) { // 如果prev为null表示删除是头节点,否则就不是头节点
first = next;
} else {
prev.next = next;
x.prev = null; // 指定节点的前继节点置空
}
if (next == null) { // 如果next为null,则表示删除的是尾部节点,否则就不是尾部节点
last = prev;
} else {
next.prev = prev;
x.next = null; // 指定节点的后继节点置空
}
x.item = null; // 置空需删除的指定节点的值
size--; // 数量减1
modCount++;
return element;
}
remove(Object o)
与removeFirstOccurrence(Object o)
底层逻辑一致,从头部开始寻找指定元素,并移除:
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
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;
}
removeLastOccurrence(Object o)
表示从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时),从尾部开始寻找指定元素,并移除:源码如下:
public boolean removeLastOccurrence(Object o) {
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
2.4 全删
clear()
:清空链表。
public void clear() { // 清空链表
// 清除节点之间的所有链接是“不必要的”,但是:如果丢弃的节点居住了多个世代,即使有一个可达的Iterator,也可以确保释放内存,这有助于世代GC
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
3 查
3.1 通过索引 get() 随机访问
get(int index)
:获取指定位置上的元素
element()
:获取但不移除此列表的头(第一个元素)
getFirst()
:获取第一个元素(最早添加的元素)
getLast()
:获取最后一个元素(最晚添加的元素)
public E get(int index) {
checkElementIndex(index); // 元素下标的合理性检查
return node(index).item; // node(index)真正查询匹配元素并返回
}
/**
* 返回指定元素索引处的(非空)节点。
*/
//时间复杂度为n/2
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;
}
}
//得到头部元素
public E element() {
return getFirst();
}
//得到头部元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
//得到尾部元素
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
3.2 通过索引 peek() 访问
与get()
源码差不多,底层逻辑一致,区别在于peek()
能查到并返回null,而get()
会抛出异常。
// 返回头元素,并且不删除。如果不存在,返回null
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
// 返回头元素,并且不删除。如果不存在,返回null
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
// 返回尾元素,如果为null,则就返回null
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
3.3 indexOf 直接查找访问
// 返回元素在链表中的索引,如果不存在则返回-1
public int indexOf(Object o) {
int index = 0;
if (o == null) { // 返回元素在链表中的索引,如果不存在则返回-1
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else { // 返回元素在链表中的索引,如果不存在则返回-1
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
4 改
4.1 转化成数组
/**
* 以正确的顺序(从第一个元素到最后一个元素)返回包含此列表中所有元素的数组。
*
* <p>返回的数组将是“安全的”,因为此列表不保留对其的引用。(换句话说,此方法必须分配一个新数组)。 因此,调用者可以自由修改返回的数组。
*
* <p>此方法充当基于数组的API和基于集合的API之间的桥梁。
*/
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item; //按顺序存入列表节点值
return result;
}
/**
* 返回一个数组,该数组按正确的顺序包含此列表中的所有元素(从第一个元素到最后一个元素); 返回数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则将其返回。 否则,将使用指定数组的运行时类型和此列表的大小分配一个新数组。
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
//为什么用到反射==》泛型类在运行时无法new实例化,只能在运行时得到class对象
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size)
a[size] = null;
return a;
}
4.2 直接替换
public E set(int index, E element) { // 用指定的元素替换此列表中指定位置的元素。
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item; // 将指定位置的元素值赋给oldVal
x.item = element;
return oldVal;
}
队列操作
具体的操作分别对应在查询、增、删操作中,详见本文增、删、查操作具体内容。
//取队列头节点,但不删除
//若first为空时,不抛出错误
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//若first为空时,抛出错误
public E element() {
return getFirst();
}
//出队列(头删)
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//入队列(尾插)
public boolean offer(E e) {
return add(e);
}
栈操作
出入栈操作:
//入栈,头部插入
public void push(E e) {
addFirst(e); // addFirst()方法详见增加操作1.2追加头部
}
//出栈,头部删除
public E pop() {
return removeFirst(); //removeFirst()详见删除操作2.2 删除头部
}
迭代器实现
深入理解Java中的迭代器
关于快速报错fail-fast想说的之fail-fast的实现原理(一)
ListIterator接口相较于传统Iterator的区别在于:ListIterator可以实现双向的迭代访问。
ListItr实现ListIterator接口:
//从列表中的指定位置开始(按正确顺序)返回此列表中元素的列表迭代器。
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index); // 检查索引合理性
return new ListItr(index);
}
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned; // 上一次返回的next或previos 的节点
private Node<E> next; //下一个节点
private int nextIndex;
private int expectedModCount = modCount; //expectedModCount:期望版本号;modCount:当前版本号
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index); // 当前节点赋值给next
nextIndex = index;
}
// 从头到尾的迭代,获取当前节点的下一节点
public boolean hasNext() { // 判断还有没有下一个元素
return nextIndex < size;
}
public E next() { // 获取下一个元素
checkForComodification(); // 检查版本号
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next; // lastReturned赋值为当前节点
next = next.next; // next赋值下一个节点,为下次迭代做准备
nextIndex++;
return lastReturned.item; //返回此次所需返回的节点的值
}
// 从尾到头的迭代,获取当前节点的上一节点
public boolean hasPrevious() { // 判断还有没有上一个元素
return nextIndex > 0;
}
public E previous() { // 获取上一个元素
checkForComodification(); // 检查版本号
if (!hasPrevious())
throw new NoSuchElementException();
// next为空,说明是第一次迭代,取尾节点(last)
// next不为空,说明已经发生过迭代了,取前一个节点即可(next.prev)
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
public void remove() {
checkForComodification();
if (lastReturned == null) // 未检查是否还有节点就remove,lastReturned返回初始null值
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned); // 删除当前节点
// 想要满足next == lastReturned,只有一种情况,就是目前是以从尾到头的方式进行迭代的,这时候由于lastReturned节点删除了,会被GC回收,就需要把next指向lastReturned.next位置,这样进行下一次previous时,才可以找到lastReturned.prev
if (next == lastReturned) // 从尾到头迭代时,lastReturned被删除回收时发生
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
总结
LinkedList和ArrayList的区别
结构:
- ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。
- LinkedList的占用的内存空间较大,需要创建node对象,需要建立前后关联
- LinkedList集合不支持 高效的随机随机访问(Random Access),因为可能产生二次项的行为,而ArrayList支持随机访问且速度较快。
速度 :
- LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Entry的引用地址,故而,LinkedList修改删除较快,查询较慢。
- ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址,故而,ArrayList修改删除较慢,查询较快。
线程安全性:
ArrayList安全,LinkedList不安全
选择
一般,查询用ArrayList,增删用LinkedList。另外:
1、如果十分确定插入、删除的元素是在前半段,使用LinkedList
2、如果十分确定插入、删除的元素后半段,使用ArrayList
3、如果上面的两点不确定,建议使用LinkedList
说明:其一、LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;其二、插入元素的时候,弄得不好ArrayList就要进行一次扩容,而ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作,所以综合来看就知道选择哪个类型的list。