LInkedList底层源码
简述
- 数据结构中存在两种数据结构,线性表和链式存储结构(链表)、
- 顺序存储结构:Vector,Stack,ArrayList
- 链式存储结构:linkedList,Queue
继承了AbstartSequentialList实现了List,Deque,Coneable,Serializable
List集合,Deque双端队列,Coneable克隆,Serializable序列化
建立持久化对象,LinkedList中的元素就是一个个节点,而真正的数据则存放在Node中
增和删除操作非常快(复杂度O(1)),查和改操作相对较慢
linkedList的操作单线程安全,多线程不安全
内部接口方法
list接口存在的方法
public interface List<E> extends Collection<E> {
...
// 增
boolean add(E e);
void add(int index, E element);
// 删
boolean remove(Object o);
E remove(int index);
// 改
E set(int index, E element);
// 查
E get(int index);
...
}
Linkedlist的成员变量
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
// 序列化唯一表示 UID
private static final long serialVersionUID = 876323262645176354L;
// LinkedList的大小,其实就是其内部维护的双向链表存储元素的数量
transient int size = 0;
// 头结点,指向第一个节点的指针或引用,默认为 null
transient Node<E> first;
// 尾节点,指向最后一个节点的指针或引用,默认为 null
transient Node<E> last;
...
}
transient防止序列化,
分别存在链表长度,头节点,尾节点
构造函数
public LinkedList() {
}
无参构造
public LinkedList(Collection<? extends E> c) {
// 指向无参的构造函数
this();
addAll(c);
}
有参构造
构造包含指定元素的列表集合
内部的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;
}
}
- 因为LInkedList是双向链表,故而从下面的Node节点的两个指针数据,一个指向向上的节点,一个指向向下的节点,相似于ArrayList源码中的next和province
这里的Node必须是静态的,Node在LInkedList类中是一个内部类,若不使用static修饰,那么Node类就是一个普通的内部类,在java中一个普通的内部类在实例化后,默认会引起外部类的引用,这就有可能造成内存的泄露
第一步:E item存储的元素 next指向下一个节点,prev指向上一个节点,内部类的构造方法
增加add方法()
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
public boolean add(E e) {
linkLast(e);
return true;
}
add(int index, E element)首先add方法两个参数,index为下标参数,element为元素参数
- 第一步进行使用checkPositionIndex方法,查看当前节点位置是否存在,如果不存在,则抛出异常
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
对元素的位置下标进行判断
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
- 第二步对下标位置和集合长度进行比对,这里的index应该为指针,进行对链表尾部的扩容
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++;
}
在链表尾部进行尾插法扩容
- 第三步如果不相等的话
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
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++;
}
在链表中进行断链添加数据再生成新的节点
- 第四步:nextIndex此处为后移指针,expectedModCount为当前元素的指针
boolean add(E e)进行尾节点判断
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;
if (numNew == 0)
return false;
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
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;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
boolean addAll()
addAll对象为Collection的集合对象,返回值为下面的方法
首先检查这个下标是否符合,将获得到的集合复制为Object类型
获取转为数组的长度,并进行判断,是否等于0,如果等于0,则该集合在添加过程数据被进行了修改
定义前置节点和后置节点,然后判断是否是链表尾部,如果是,在链表尾部追加数据
尾部的后置节点一定是null,前置节点是队尾,如果不在链表的末端,在链表的中间,则取出index节点,并作为后继节点,index的前节点,作为节点前驱
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;
}
}
然后进行循环依次增加,靠for循环遍历数组,依次指向节点的插入操作,进行类型转换,如果前置节点为空,则newNode为头节点,否则为pred的next的节点
当循环结束,如果后置节点为null,说明此时是在队尾追加,否则实在堆中加入的,则需要更新前置节点和后置节点,然后对数量进行修改
node方法,首先是取出index节点,如果index小于size/2/,则从头部进行寻找,将头节点赋值给x,然后进行遍历,如果index大于等于size/2则从后开始遍历,然后检测index的位置是否合法
addFirst(E e)将元素添加到连头尾部
public void addFirst(E e) {
linkFirst(e);
}
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++;
}
首先建立一个新的节点,并以头节点为后继节点,然后进行判断,如果链表尾空,则last也指向该节点,否则,将头节点的前去指针指向新节点,也就是指向前一个元素
addLast(E e)将元素添加到链表的尾部
public void addLast(E e) {
linkLast(e);
}
获取数据的方法get
public E get(int index) {
//检查index范围是否在size之内
checkElementIndex(index);
//调用Node(index)去找到index对应的node然后返回它的值
return node(index).item;
}
检查idnex是否在size中,调用Node(index)找到index对应的node并返回值
获取头节点的数据方法getFirst
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E element() {
return getFirst();
}
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
getFirst会在头节点为空时抛出异常,而element内部用的getFirst方法;peek和peekFirst方法在头结点为空时返回null
- 第一步:定义头节点,如果为空抛出异常,并返回当前节点
- 第二步:返回第一个节点
获取尾节点的数据getLast()
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
第一步:getLast()定义尾节点,如果为空,抛出异常,并返回当前节点
这两者的区别也在尾节点为空时是抛出异常还是返回null
根据对象获取得到index方法
int index(Object o)
从头遍历找
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;
}
- 第一步:定义指针位置(初始为0)
- 第二步:对传入对象进行为空判断,传入节点不为空时,进行遍历,定义新节点等于第一个节点,判断条件节点为不为空,遍历节点不断向后,再进行判断,如果遍历当前节点为空时,则返回指针位置,传入节点为空时,同上
int lastIndexOf(Object o): 从尾遍历找
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;
}
contains(Object o): 检查对象o是否存在于链表中
public boolean contains(Object o) {
return indexOf(o) != -1;
}
从indexOf()中可以看出如果没有此Object会返回-1
删除方法
remove() ,removeFirst(),pop(): 删除头节点
public E pop() {
return removeFirst();
}
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
removeLast(),pollLast(): 删除尾节点
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
remove(Object o): 删除指定元素
public boolean remove(Object o) {
//如果删除对象为null
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;
}
unlink(Node x) 方法:
E unlink(Node x) {
// assert x != null;
final E element = x.item;
final Node next = x.next;//得到后继节点
final Node 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;
}
本质就是将一个节点删除,然后将原来的前驱节点与原来的后继节点连接起来
remove(int index):删除指定位置的元素
public E remove(int index) {
//检查index范围
checkElementIndex(index);
//将节点删除
return unlink(node(index));
}
经过一番学习和测试以后,得出以下结论:这些方法从设计之初,分别来自于集合Collections,队列Queue,栈Stack,双端队列Deque,因此它们是有语义的,不建议笼统归为添加/删除。
add
和remove
是一对,源自Collection
;offer
和poll
是一对,源自Queue
;push
和pop
是一对,源自Deque
,其本质是栈(Stack
类由于某些历史原因,官方已不建议使用,使用Deque
代替);offerFirst/offerLast
和pollFirst/pollLast
是一对,源自Deque
,其本质是双端队列。
那为什么这些方法,全都出现在LinkedList/Deque
中呢,那是由它们的继承关系导致的,请看下图。
image.png
关注圈住的部分,接口Deque
继承了以上所有的方法,而类LinkedList
实现了以上所有的方法。
注:由于历史原因,在Java中,官方不建议使用Stack类,而是使用Deque
代替,也就是说,接口Deque
是栈和双端队列这两种数据结构的集合体。
说了这么多,这一堆方法到底有什么区别?其实从他们的出处便可以快速区分并且牢记他们的不同之处。
add/remove
源自集合,所以添加到队尾,从队头删除;offer/poll
源自队列(先进先出 => 尾进头出),所以添加到队尾,从队头删除;push/pop
源自栈(先进后出 => 头进头出),所以添加到队头,从队头删除;offerFirst/offerLast/pollFirst/pollLast
源自双端队列(两端都可以进也都可以出),根据字面意思,offerFirst
添加到队头,offerLast
添加到队尾,pollFirst
从队头删除,pollLast
从队尾删除。
总结:add/offer/offerLast
添加队尾,三个方法等价;push/offerFirst
添加队头,两个方法等价。remove/pop/poll/pollFirst
删除队头,四个方法等价;pollLast
删除队尾。
虽说某几个方法等价,但是我们在使用的时候,建议根据用途来使用不同的方法,比如你想把LinkedList
当做集合list
,那么应该用add/remove
,如果想用作队列,则使用offer/poll
,如果用作栈,则使用push/pop
,如果用作双端队列,则使用offerFirst/offerLast/pollFirst/pollLast
。根据语义使用,就不会发生:我想删队尾,结果删了队头这种事了。
出 => 尾进头出),所以添加到队尾,从队头删除;
push/pop
源自栈(先进后出 => 头进头出),所以添加到队头,从队头删除;offerFirst/offerLast/pollFirst/pollLast
源自双端队列(两端都可以进也都可以出),根据字面意思,offerFirst
添加到队头,offerLast
添加到队尾,pollFirst
从队头删除,pollLast
从队尾删除。
总结:add/offer/offerLast
添加队尾,三个方法等价;push/offerFirst
添加队头,两个方法等价。remove/pop/poll/pollFirst
删除队头,四个方法等价;pollLast
删除队尾。
虽说某几个方法等价,但是我们在使用的时候,建议根据用途来使用不同的方法,比如你想把LinkedList
当做集合list
,那么应该用add/remove
,如果想用作队列,则使用offer/poll
,如果用作栈,则使用push/pop
,如果用作双端队列,则使用offerFirst/offerLast/pollFirst/pollLast
。根据语义使用,就不会发生:我想删队尾,结果删了队头这种事了。