LinkedList
文章目录
1. 简介
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{}
LinkedList
是List
接口的实现类,继承了AbstractSequentialList
,并实现了List
,Deque
和Cloneable
接口。它是一个集合,可以根据索引来随机访问集合中的元素,也因为实现了Deque
接口,他还是一个队列,可以被当成双端队列使用,虽然LinkedList
是一个List
集合,但是它的实现方式和ArrayList
是完全不同的,ArrayList的底层是通过一个动态的Object[]数组
来实现的,LinkedList的底层是通过链表
来实现的,因此他的随机访问效率差,但是添加,删除效率高。
总结一下:
- LinkedList是基于双向链表实现的,除了可以当做链表来操作外,它还可以当做栈、队列和双端队列来使用。
- LinkedList同样是非线程安全的,只在单线程下适合使用。
- LinkedList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了Cloneable接口,能被克隆。
2. 成员变量
transient int size = 0;//LinkedList中存放的元素个数
transient Node<E> first;//头节点
transient Node<E> last;//尾节点
大家看到Node这个类型可能不太懂是什么意思,其实这是Linked自己封装的一个内部类,相当于链表中的一个节点。
//静态内部类,创建节点
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;
}
}
什么是链表
链表是由
一系列非连续的节点
组成的数据结构,简单分下类的话,链表又分为单向链表和双向链表
,而单向/双向链表又可以分为循环链表和非循环链表
,下面简单就这四种链表进行图解说明。
1. 单向链表
单向链表就是通过每个节点的指针指向下一个节点从而链接起来的结构,最后一个节点的next指向null。
2. 单向循环链表
单向循环链表和单向列表的不同是,最后一个节点的next不是指向null,而是指向head节点,形成一个“环”。
3. 双向链表
从名字就可以看出,双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。
而LinkedList就是基于双向链表设计的。
4. 双向循环链表
双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。
总结:
LinkedList是通过双向链表来实现的,既然是链表那么随机访问的效率比ArrayList低,但是添加和删除的效率比ArrayList高(ArrayList尾部添加元素的效率和LinkedList差不多),大家还需要了解两个概念,不然看源码大家可能会有点迷惑。
- 前驱(prev):每一个节点都有前驱,指向前面节点的指针
- 后继(next):每一个节点都有后继,指向后面节点的指针
- 元素或者值(item):就是每个节点存储的元素。
3. 构造函数
//构造方法,创建一个空的列表
public LinkedList() {}
//将一个指定的集合添加到LinkedList中,先完成初始化,在调用添加操作
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
可以从LinkedList提供的两个构造函数看出没有长度这个概念,所以不存在容量不足的情况(前提条件是内存足够),因此不需要判断超出长度,使用扩容方法。
//将集合中的元素添加到List中
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
//将集合中的元素全部插入到LinkedList中,并从指定的位置开始
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);//检测索引是否合法
Object[] a = c.toArray();//将Collection集合转化为数组
int numNew = a.length;//数组长度
if (numNew == 0)//集合中没有元素,返回false
return false;
Node<E> pred, succ;//pred 前驱节点 succ:当前节点
if (index == size) {//判断此时是集合最后元素
succ = null;//当前节点为null
pred = last;//前驱节点是尾节点
} else {
succ = node(index);//获取位置为index的结点元素,并赋值给succ
pred = succ.prev;//succ 节点的前驱节点
}
//遍历数组进行插入操作。修改节点的前驱后继
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;//如果前驱节点为null,变为头节点
else
pred.next = newNode;//后驱节点指向新增的节点
pred = newNode;//把newNode变为前驱节点,继续追加
}
if (succ == null) {
last = pred;//最后的newNode变为尾节点
} else {
pred.next = succ;//数组最后的节点后继指向添加前的节点
succ.prev = pred;//添加前的节点前驱指向数组最后的节点
}
size += numNew;
modCount++;
return true;
}
4. 常用方法解析
4.1 add操作
//在LinkedList的尾部添加元素
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;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
LinkedList添加元素默认是添加到LinkedList的尾部,首先将last节点赋值给I节点,然后新建节点newNode ,并把前驱指向I节点,data = e , next = null , 并将新节点赋值给last节点,它成为了最后一个节点,根据当前List是否为空做出相应的操作。若不为空将l的后继指针修改为newNodw。 size +1 , modCount+1
//在指定索引添加元素
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//在succ节点前插入e节点,并修改各个节点之间的前驱后继
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
//插入头节点
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
从源码中可以看到,LinkedList不仅提供了linkLast(E e)
(插入尾节点)的方法还提供了linkFirst(E e)
(插入头节点)和add(int index, E element)
(指定位置节点的方法),实现了双端操作。
4.2 remove操作
//删除指定索引的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//删除值为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;
}
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) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
无论是删除指定索引索引的元素还是值为o的元素,底层都是调用 unlink(Node<E> x)
方法。remove(Object o)
先循环遍历列表,找到item == o 的节点,在调用unlink()方法删除。而删除指定索引的元素可以直接找到o节点,然后删除。
4.3 set操作和get操作
//将节点放置在指定的位置
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
//返回指定元素的值
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
返回指定位置的节点信息
LinkedList无法随机访问,只能通过遍历的方式找到相应的节点
为了提高效率,当前位置首先和元素数量的中间位置开始判断,小于中间位置,
从头节点开始遍历,大于中间位置从尾节点开始遍历
*/
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;
}
}
因为LinkedList无法随机访问,只能通过遍历查找元素,虽然通过加速,只要查找一半的集合。找到指定的元素然后修改其值。
4.4 clear操作
public void clear() {
//遍历集合中元素,然后置为null。
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++;
}
5. 总结
- LinkedList是一个功能很强大的类,可以被当作List集合,双端队列和栈来使用。
- LinkedList底层使用链表来保存集合中的元素,因此随机访问的性能较差,但是插入删除时性能非常的出色。
- ArrayList,底层采用数组实现,如果需要遍历集合元素,应该使用随机访问的方式,对于LinkedList集合应该采用迭代器的方式。
- 如果需要经常的插入。删除操作可以考虑使用LinkedList集合
如果有多个线程需要同时访问List集合中的元素,开发者可以考虑使用Collections将集合包装成线程安全的集合。
6. 栈和队列的源码分析
//弹出第一个元素的值
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取第一个元素
public E element() {
return getFirst();
}
//弹出第一元素,并删除
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//删除第一个元素
public E remove() {
return removeFirst();
}
//添加到尾部
public boolean offer(E e) {
return add(e);
}
//添加到头部
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
//插入到最后一个元素
public boolean offerLast(E e) {
addLast(e);
return true;
}
//队列操作
//尝试弹出第一个元素,但是不删除元素
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//队列操作
//尝试弹出最后一个元素,不删除
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
//弹出第一个元素,并删除
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//弹出最后一个元素,并删除
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
//如队列,添加到头部
public void push(E e) {
addFirst(e);
}
//出队列删除第一个节点
public E pop() {
return removeFirst();
}
//删除指定元素第一次出现的位置
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}