概述
上一篇「ArrayList源码深入解析」文章中,我们详细讲解了ArrayList
的构造方法和常用的API的源码,今天来讲一下它的兄弟LinkedList
,这两个类经常会拿来作比较。
它们俩可以说是List中两种完全不同的实现,ArrayList
属于线性表结构,而LinkedList
属于链表结构。
ArrayList
的增删效率低,但是改查的效率高。LinkedList
正好相反,由于它是属于链表结构,所以增删时不需要移动底层数组数据,而是只需要修改链表节点指针的指向即可,因此效率高。而改和查需要先定位到下标节点,所以效率低。
概要
LinkedList的实现原理是链表,它也是线程不安全的,允许其中元素为null。
其底层结构是双向链表,它实现了List<E>, Deque<E>, Cloneable, java.io.Serializable
接口,Deque<E>
接口表示它可以作为一个双端队列。它没有实现RandomAccess
接口,所以不具有随机快随访问能力。
因其底层为链表结构,它的增删只需要移动指针即可,不需要像ArrayList
需要扩容和复制数组,所以时间和空间效率较高。
而缺点就是随机访问元素时,时间效率较低,随着数据量的增加,时间效率会不断降低。
构造方法详解
//集合大小
transient int size = 0;
//链表的第一个节点
transient Node<E> first;
//链表的最后一个节点
transient Node<E> last;
//默认构造方法
public LinkedList() {
}
//将集合c的全部元素插入链表中
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
比起ArrayList的构造方法简单了很多。
Node 节点结构
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;
}
}
从节点可以看出这是一个双向链表,prev为当前节点的前一个节点,next为当前节点的后一个节点,item为当前节点的值。
常用API详解
1 添加元素
addAll,添加多元素
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
//检查下标是否越界
checkPositionIndex(index);
//获取集合的数组对象
Object[] a = c.toArray();
int numNew = a.length;
//判断添加数量是否为0
if (numNew == 0)
return false;
//index节点的前置节点和后置节点
Node<E> pred, succ;
//如果index == size,则在链表尾部插入
if (index == size) {
//后置节点一定为null
succ = null;
//前置节点为尾节点
pred = last;
} else {
//取index节点为尾节点
succ = node(index);
//前置节点为index节点的前一个节点
pred = succ.prev;
}
//链表批量添加是靠for循环遍历原数组,一次执行插入节点操作
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//以前置节点和元素e构建新节点
Node<E> newNode = new Node<>(pred, e, null);
//判断如果前置节点为null说明是首节点
if (pred == null)
//将新节点设置为首节点
first = newNode;
else
//将新节点设置为下一个节点的前置节点
pred.next = newNode;
//将新节点设置为前置节点
pred = newNode;
}
//循环结束后,判断后置节点为null,说明succ是尾节点
if (succ == null) {
//则将最后添加的节点设置为尾节点
last = pred;
} else {
//将当前节点的后置节点设置为succ
pred.next = succ;
//将succ的前置节点设置为当前节点
succ.prev = pred;
}
//修改size
size += numNew;
modCount++;
return true;
}
//根据index 查询出Node对象
Node<E> node(int index) {
//通过查询下标计算出当前下标是属于前半段还是后半段,如果是前半段就从首结点开始查询,如果是后半段就从尾结点开始查询
if (index < (size >> 1)) { //查询下标在前半段
//获取首结点
Node<E> x = first;
//查询index下标对应的节点对象
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;
}
}
//检查下标越界
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//检查下标是否越界
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
小结
1 链表批量添加是靠for循环遍历数组依次执行插入节点操作。
2 通过下标获取节点是,会根据下标位于前半段还是后半段来确定是从首节点还是尾节点开始搜索。
add,添加单个元素
//添加单个元素
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;
//如果尾节点为null,说明是第一次插入,所以这个节点既是首节点也是尾节点
if (l == null)
first = newNode;
else
l.next = newNode;
//修改集合大小
size++;
modCount++;
}
//在指定下标位置插入元素
public void add(int index, E element) {
//检查下标越界
checkPositionIndex(index);
//判断下标是否=size,如果等于直接在尾部插入
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//在succ节点前插入一个新节点e
void linkBefore(E e, Node<E> succ) {
// 获取index位置节点的前置节点
final Node<E> pred = succ.prev;
//将index位置节点设置为插入节点的后置节点
final Node<E> newNode = new Node<>(pred, e, succ);
//将index节点的前置节点设置为新节点
succ.prev = newNode;
//如果index节点的前置节点为null,则为首节点
if (pred == null)
//则将新节点设置为首节点
first = newNode;
else
//将succ的前置节点的后置节点设置为新节点
pred.next = newNode;
size++;
modCount++;
}
2 删除元素
删除指定下标的元素
//删除指定下标元素
public E remove(int index) {
//检查下标越界
checkElementIndex(index);
return unlink(node(index));
}
//删除结点x
E unlink(Node<E> x) {
// 当前节点的元素值
final E element = x.item;
//当前节点的购置节点
final Node<E> next = x.next;
//当前节点的前置节点
final Node<E> prev = x.prev;
//判断前置节点是否为null,如果为null,则当前节点为首节点
if (prev == null) {
//将首节点设置为当前节点的后置节点
first = next;
} else {
//将当前节点的前置节点的后置节点设置为,当前节点的后置节点
prev.next = next;
//把当前节点的前置节点设置为null
x.prev = null;
}
//判断当前节点的后置节点是否为null,如果为null,则当前节点为尾节点
if (next == null) {
//将尾节点设置为当前节点的前置节点
last = prev;
} else {
//将当前节点的后置节点的前置节点设置为当前节点的前置节点
next.prev = prev;
//将当前节点的后置节点设置为null
x.next = null;
}
//将当前节点元素设置为null
x.item = null;
//修改size
size--;
modCount++;
return element;
}
删除链表中指定元素节点
//因为存在元素值为null的情况所以分情况判断
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;
}
小结
按下标删除,也是先根据index找到Node节点,然后从链表上unlink掉这个节点。按元素删除实现遍历找到要删除的Node节点,考虑到允许为null,所以需要遍历两编,然后再unlike掉节点。
3 修改元素
//修改节点
public E set(int index, E element) {
//检查下标越界
checkElementIndex(index);
//通过下标获取节点
Node<E> x = node(index);
//获取节点旧元素值
E oldVal = x.item;
//修改节点元素值
x.item = element;
//返回旧值
return oldVal;
}
4 查询元素
根据index查询节点
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
根据节点元素值查询下标
//通过元素值获取下标
public int indexOf(Object o) {
int index = 0;
//分情况考虑
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) {
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;
}
总结
LinkedList是双向链表结构。
- 链表批量添加是靠for循环遍历数组,依次执行节点插入。
- 通过下标获取Node节点时,会根据index处于链表中的位置来确定是头部还是尾部开始遍历。
- 按下标进行删除时,实现根据index找到Node节点,然后再
unlike
掉节点。 - 所以
LinkedList
增删的效率高,改和查的效率要低。