LinkedList是Java中常用的一个集合类,是List接口的一个实现类,而List接口继承自Collection接口,所以LinkedList是Collection的一个实现类。
本篇主要讨论一下LinkedList底层代码的实现。
核心成员变量
//size表示linkedList的大小,即该linkedList已经存储了多少个元素
transient int size = 0;
//由于linkedList是由链表实现的,该first表示链表的第一个节点
transient Node first;
//由于linkedList是由链表实现的,该list表示链表的最后个节点
transient Node last;
Node的实现
//该Node是由双链表的实现
private static class Node {
//该节点表示实际存储的元素
E item;
//该节点的下一个节点
Node next;
//该节点的上一个节点
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList无参构造函数
//该构造函数实际什么也没有做
public LinkedList() {
}
add(E e)方法的实现
//该方法向链表尾部添加一个元素,并返回true
public boolean add(E e) {
//链表尾部添加元素
linkLast(e);
return true;
}
//向链表尾部添加一个元素
void linkLast(E e) {
//使用一个局部变量指向链表里面的最后一个元素
final Node l = last;
//新创建一个node节点,使该新建的节点的上一个节点指向之前链表中的最后一个节点
final Node newNode = new Node<>(l, e, null);
//因为是在尾部添加节点,所以新创建的节点赋予last变量,表示last指向链表中的最后一个几点
last = newNode;
//如果l为空,说明之前的链表中的节点元素为空
if (l == null)
//所以新创建的节点赋予first变量,到这里表示链表中仅有一个节点元素,所以该节点元素即是第一个节点,也是最后一个节点
first = newNode;
else
//如果l不为空,说明之前的链表不为空,newNode又是最后一个节点,又因为l为之前旧链表的最后一个节点,所以l的下一个节点指向当前新建的节点
l.next = newNode;
//size+1,用来表示当前数组已经存储了多少个节点元素
size++;
modCount++;
}
具体说明:
- 当往LinkedList添加元素时,实际在链表最后添加一个节点;
- 当往链表中添加第一个节点时,由于链表中仅有一个节点,所以first指向该节点,last也指向该节点;
- 当往链表中添加第二个节点时,由于链表中已经有一个节点,所以first还是指向链表中的第一个节点,而新建的第二个节点的prev指向第一个节点,last指向新建的第二个节点,并建第一个节点的next指向第二个节点;
如图所示:
remove()方法的实现
public E remove() {
//删除链表中的第一个节点
return removeFirst();
}
public E removeFirst() {
//获取链表的第一个节点
final Node f = first;
if (f == null)
throw new NoSuchElementException();
//删除第一个的节点,并返回删除的数据
return unlinkFirst(f);
}
private E unlinkFirst(Node f) {
//获取第一个节点存储的数据,用于返回
final E element = f.item;
//获取第一个节点的之后的下一个节点,第一个节点被删除后,下一个节点就是first节点
final Node next = f.next;
//清空第一个节点的信息,即清空要删除节点的信息
f.item = null;
f.next = null;
//将原来第一个节点的下一个节点赋予first引用,也就是原来第二个节点变为链表的第一个节点
first = next;
//如果原来第一个节点的下一个节点为空,那么说明链表此时已经为空了
if (next == null)
//此时,将last也置空
last = null;
else
//原来第二个节点变为第一个节点,因为第一个节点前面没有节点了,所以将第一个节点的prev置空
next.prev = null;
//size-1,表示链表存储的数据-1
size--;
modCount++;
//返回删除的元素
return element;
}
具体说明:
- 该方法将删除链表中的第一个节点,并将删除的数据返回。
remove(Object o)方法实现
//删除指定元素的节点,删除成功,返回true,否则,返回false
public boolean remove(Object o) {
if (o == null) {
//如果要删除的元素为null,那么将删除链表中第一个为null的节点
for (Node x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
//删除链表中第一个与传入值相等的节点
for (Node x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
//删除指定节点
E unlink(Node x) {
//获取要删除节点的元素的值,用于删除后返回
final E element = x.item;
//获取要删除节点的下一个节点
final Node next = x.next;
//获取要删除节点的上一个节点
final Node prev = x.prev;
if (prev == null) {
//如果要删除的上一个节点为空,说明当前要删除的节点为第一个节点
//那么需要将first指向要删除节点的下一个节点
first = next;
} else {
//将要删除的节点的上一个节点的引用指向要删除节点的下一个节点
//比如原来是1,2,3 那么删除2后,就变成 1,3 也就是将1指向3
prev.next = next;
//并将删除的节点的上一个指针置null,也就是将2指向1的指针置null
x.prev = null;
}
if (next == null) {
//如果要删除的下一个节点为空,说明当前要删除的节点为最后节点
//那么需要将last指向要删除节点的上一个节点
last = prev;
} else {
//将要删除的节点的下一个节点的引用指向要删除节点的上一个节点
//比如原来是1,2,3 那么删除2后,就变成 1,3 也就是将3指向1
next.prev = prev;
//并将删除的节点的下一个指针置null,也就是将2指向3的指针置null
x.next = null;
}
//清空删除节点的数据
x.item = null;
//size-1,表示删除了一个数据
size--;
modCount++;
//返回删除的节点的元素
return element;
}
具体说明:
- 传入要删除的数据,然后遍历链表,删除第一个匹配的数据节点;
remove(int index)方法实现
//删除指定索引下标的元素
public E remove(int index) {
//首先,检查传入的索引下标是否合法,不合法将抛出IndexOutOfBoundsException
checkElementIndex(index);
//node方法根据index取出要删除的节点,unlink在上面已经分析过,就是删除指定节点并返回该节点的数据
return unlink(node(index));
}
//检查索引是否合法
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//传入索引必须大于等于0小于size
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
//根据索引下标,变量链表,获取指定下标的节点
Node node(int index) {
//判断如果要删除的索引小于size/2,则从链表左边开始遍历查找节点
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//判断如果要删除的索引大于size/2,则从链表右边开始遍历查找节点
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
具体步骤:
- 传入要删除的索引下标,首先检查该索引下标是否合法;
- 如果下标合法,则调用node(index)查询要删除的节点,如果要删除的索引下标在size的左半部分(index < size/2),那边将从左边(first)开始遍历链表查询,反之从右边(last)开始查找;
- 获取到要删除的节点时,调用unlink(node)删除节点,并返回节点数据。
set(int index, E element)方法实现
//修改指定索引下标的节点的值,并返回修改前的值
public E set(int index, E element) {
//检查index是否合法
checkElementIndex(index);
//获取到index的节点
Node x = node(index);
//获取到指定下标节点的旧的值
E oldVal = x.item;
//替换指定下标节点的值
x.item = element;
//返回指定下标节点旧的值
return oldVal;
}
具体步骤:
- 传入要修改的节点的索引下标与要修改的值;
- 检查下标是否合法;
- 调用node(index)获取指定下标的节点;
- 获取指定下标节点的值,并为该节点设置新的值;
- 返回指定下标节点替换前的值。
get(int index)方法实现
//根据Index获取数据
public E get(int index) {
checkElementIndex(index);
//调用node获取数据
return node(index).item;
}
具体说明:
- 可以看到,其实get(index)这种方式获取数据,也是通过调用node(index)去获取数据;
适用场景
通过源码的分析,我们知道,linkedList底层是基于链表实现的,每个添加一个元素的时候,都是在链表的尾部添加节点,不涉及到链表的循环遍历;当删除元素时,如果调用无参的remove方法时,只是删除链表的第一个节点,也不涉及到链表的循环遍历;而当需要通过索引下标获取数据的时候,涉及到了链表的遍历;所以在这种添加与删除节点比较多而根据索引去访问元素比较少的场景下,比较适合使用linkedList。