LinkedList
UML图
- 继承AbstractSequentialList
- 实现List, Deque, Cloneable, java.io.Serializable
总结
1、LinkedList底层是一个双链表。是一个直线型的链表结构。
2、LinkedList是线程不安全的。
3、遍历速度相对ArrayList要慢。在遍历LinkedList的时候,官方更推荐使用顺序访问,也就是使用我们的迭代器。(因为LinkedList底层是通过一个链表来实现的)(虽然LinkedList也提供了get(int index)方法,但是底层的实现是:每次调用get(int index)方法的时候,都需要从链表的头部或者尾部进行遍历,每一的遍历时间复杂度是O(index),而相对比ArrayList的底层实现,每次遍历的时间复杂度都是O(1)。所以不推荐通过get(int index)遍历LinkedList。至于上面的说从链表的头部后尾部进行遍历:官方源码对遍历进行了优化:通过判断索引index更靠近链表的头部还是尾部来选择遍历的方向)(所以这里遍历LinkedList推荐使用迭代器)。
4、支持克隆(浅克隆),底层实现:LinkedList节点并没有被克隆,只是通过Object的clone()方法得到的Object对象强制转化为了LinkedList,然后把它内部的实例域都置空,然后把被拷贝的LinkedList节点中的每一个值都拷贝到clone中。
5、支持序列化。(和ArrayList一样,底层都提供了两个方法:readObject(ObjectInputStream o)、writeObject(ObjectOutputStream o),用于实现序列化,底层只序列化节点的个数和节点的值)。
6、插入或删除元素的效率要比ArrayList快,因为ArrayList每次执行插入或者删除操作的时候需要移动底层数组中元素的位置。
7、LinkedList内部实现了6种主要的辅助方法:void linkFirst(E e)、void linkLast(E e)、linkBefore(E e, Node succ)、E unlinkFirst(Node f)、E unlinkLast(Node l)、E unlink(Node x)。它们都是private修饰的方法或者没有修饰符,表明这里都只是为LinkedList的其他方法提供服务,或者同一个包中的类提供服务。在LinkedList内部,绝大部分方法的实现都是依靠上面的6种辅助方法实现的,所以,只要把这6个辅助方法自己实现了(或者理解了(最好还是自己实现一次)),LinkedList的基本操作也就掌握了。
[参考博客]: https://blog.csdn.net/m0_37884977/article/details/80467658)
数据结构核心内部类Node
Node是LinkedList的内部类,用来存储元素的数据结构。
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; // 记录LinkedList的大小
transient Node<E> first; // 表示LinkedList的头节点
transient Node<E> last; // 表示LinkedList的尾节点
构造器
LinkedList提供了两个构造器,ArrayList比它多提供了一个通过设置初始化容量来初始化类。LinkedList不提供该方法的原因:因为LinkedList底层是通过链表实现的,每当有新元素添加进来的时候,都是通过链接新的节点实现的,也就是说它的容量是随着元素的个数的变化而动态变化的。而ArrayList底层是通过数组来存储新添加的元素的,所以我们可以为ArrayList设置初始容量(实际设置的数组的大小)。
1、默认构造器
public LinkedList() {
}
2、Collection集合参数构造器
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
addAll(int index, Collection<? extends E> c),在指定位置添加集合,这个方法很重要
public boolean addAll(int index, Collection<? extends E> c) {
// 判断index下标是否有效,几乎所有的涉及到在指定位置添加或者删除或修改操作都需要判断传进来的参数是否合法
checkPositionIndex(index);
// 将传进来的集合转换成数组
Object[] a = c.toArray();
int numNew = a.length;
// 如果numNew=0表示待添加的集合为空,后续代码不执行
if (numNew == 0)
return false;
// succ表示待添加节点的位置,pre表示待添加节点的上一节点
// 1、待添加的元素位于LinkedList最后一个元素的后面
// 如果index==size;说明此时需要添加LinkedList中的集合中的每一个元素都是在
// LinkedList最后面。所以把succ设置为空,pred指向尾节点。
// 2、待添加的元素位于LinkedList中间位置
// 则succ指向插入待插入位置的节点。这里用到了node(int index)方法,
// 这个方法主要是返回对应索引位置上的Node(节点)。pred指向succ节点的前一个节点。
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 接着遍历待添加的元素数组,在每次遍历的时候,都新建一个节点,
// 该节点的值存储数组a中遍历的值,该节点的prev用来存储pred节点,next设置为空。
// 如果pred==null,表示当前待添加节点的前一个节点为空,则当前待添加节点为头节点。
// 否则的话把当前待添加节点的前一个节点指向当前节点。
// 最后把pred指向当前节点,以便后续节点添加
for (Object o : a) {
@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;
}
// 当succ==null(也就是新添加的节点位于LinkedList集合的最后一个元素的后面),
// 通过遍历上面的a的所有元素,此时pred指向的是LinkedList中的最后一个元素,所以把
// last指向pred指向的节点。
// 当不为空的时候,表明在LinkedList集合中添加的元素,需要把pred的next指向succ上,
// succ的prev指向pred。
// 最后把集合的大小设置为新的大小。
// modCount(修改的次数)自增。
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
// 这里可以理解为,简单采用二分法,节省了for循环时间。
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;
}
}
常用方法
linkFirst(E e)
将参数添加到链表的头部。
// 新建f节点,指向头节点
// 新建一个节点,并将原头节点替换为新建节点
// 如果f==null表示LinkedList链表为空,则尾节点指向新节点
// 否则f.prev指向newNode
// size和modCount自增。
private