文章目录
导言:
在实际应用开发的过程中,对于数据的操作我们常常考虑这样的问题:需要快速搜索成千上万个有序序列吗?需要快速插入删除有序序列吗?需要建立键值之间的关联吗?当非常关注性能时,选择不同的数据结构会带来很大的差异。
前言
LinkedList在java集合中的位置
LinkedList是一种屋里存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点组成,结点可以在运行时动态生成,LinkedList中有两个指针,前指针和后指针,是一个双向链表;
链表的增删由于不需要移动底层数组数据,其底层是链表实现的,只需要修改链表节点指针,所以效率较高。
一、LinkedList的继承和接口实现
源码(jdk 1.8)
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
- 可以看到LinkedList继承了AbstractSequentialList抽象类(已完成接口的大部分函数,部分函数根据自己的链表结构进行了重写,如toArray()),此外LinkedList实现了Deque(双向队列的接口,扩展自queue接口),对于List、Cloneable、Serializable接口在ArrayList源码介绍中有讲到,不再赘述。
- 也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue接口的类。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。
二、LinkedList底层实现
1.底层结构
结点结构源码:
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可避免序列化此字段
transient Node<E> first;
//链表的头结点
transient Node<E> last;
//链表的尾结点
- 一个结点包含了元素值和前后结点的引用,对于首节点其前结点为NULL,对于尾结点其后结点为NULL
- 对于一个链表我们只需知道其首尾结点即可
2.增删改查
LinkedList 增加元素方法
- addFirst 在首结点之前
- addLast 在尾结点之后
- add 在尾结点之后,可提供index参数指定位置
- addAll 增加参数集合中全部元素到尾结点之后
- offer 调用add()
- offerFirst 调用addFirst()
- offerLast 调用addLast ()
- push 调用addFirst()
- 总结下来,虽然函数多,其内部实现是调用的,如offerFirst 、push 内部均是调用了 addFirst()函数
addFirst函数源码:
public void addFirst(E e) {
linkFirst(e); //调用了linkFirst
}
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++; //操作数增加
}
addAll源码
public boolean addAll(int index, Collection<? extends E> c) {//在index位置处,加入集合c全部元素
checkPositionIndex(index);//索引是否出界
Object[] a = c.toArray(); //集合转换为数组形式
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ; //记录被增加元素的前后结点
if (index == size) {//相当于从链表尾结点开始加入元素
succ = null;
pred = last;
} else {//否则
succ = node(index);//这里的node函数是获取索引为index的那个结点
pred = succ.prev;
}
for (Object o : a) { //for循环
@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) { //如果源链表index位置处的后一个结点为null
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
删除和增加对应即可,后面讲解一下查询
- peek
- getFirst :头结点值
- getLast :尾结点值
- get :参数index 获取index处的结点值
- peek :头结点值
- element :调用getFirst
- peekFirst:头结点值
- peekLast :尾结点值
- pollFirst :头结点值,并删除头结点
- pollLast :尾结点值,并删除尾结点
- pop :头结点值,并删除头结点
- lastIndexOf :查找最后一次出现的index, 如果找不到返回-1;
- indexOf :查找第一次出现的index, 如果找不到返回-1;
通过下标获取某个节点的时候,会根据index处于前半段还是后半段进行一次折半,以提升查询效率;而改和查,都需要先定位到目标节点,所以效率较低;
如下:
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { //index小于size/2,从前遍历
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其他
clear()
为了让GC更快可以回收放置的元素,需要将node之间的引用关系赋空。
public void clear() {
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++;
}
Fail-Fast机制 modCount
同ArrayList也采用了快速失败的机制,通过记录modCount参数(操作数)来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
四、另言
`
-
因为LinkedList是一个链表结构,所以可理论上存取无限的数据,但是所造成的数据访问效率低下。
-
可动态添加删除大小可变 ,内存可能是不连续内存,链式存储。
-
为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用Collections.synchronizedList()方法对其进行包装。
欢迎朋友讨论指正哈~ ~