前面学习了ArrayList与Vector源码中的常用方法,我们也知道了ArrayList与Vector的区别主要在是否同步上,接下来看看LinkedList部分的源码,LinkedList与ArrayList在面试中也常用来比较。我们都知道ArrayList增删慢改查快,因为其增删操作需要移动底层数据,而查改数据可以根据索引直接定位,效率较高;而LinkedList恰恰相反,它是增删快改查慢,因为在增删时只需要改变链表节点的指针即可,而改查过程通过定位时效率就降低了。
在源码描述里,我们可以了解到:List和Deque接口的双链表实现,实现所有可选列表操作,允许所有元素(包含null)。所有操作的执行与双向链表一样。 索引到列表中的操作将从开头或结尾遍历列表,以较接近指定索引为准。
/**
* Doubly-linked list implementation of the {@code List} and {@code Deque}
* interfaces. Implements all optional list operations, and permits all
* elements (including {@code null}).
*
* <p>All of the operations perform as could be expected for a doubly-linked
* list. Operations that index into the list will traverse the list from
* the beginning or the end, whichever is closer to the specified index.
后面的描述跟前面介绍的ArrayList相似,这里就不细说,有需要的可以移步:ArrayList源码学习(基于jdk8)。
接下来我们通过源码细节进行学习,首先我们可以看到LinkedList继承了AbstractSequentialList<E>并实现了 List<E>, Deque<E>, Cloneable, java.io.Serializable接口。从图片可以看到AbstractSequentialList<E>实现了list的主要常用接口,降低了其实现List的复杂度。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
接着看到集合的属性值:分别是集合元素个数、第一个节点、最后一个节点。
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
下面部分代码为节点的定义,可以看出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;
}
}
然后是构造方法:分别为LinkedList()和LinkedList(Collection<? extends E> c)(实际执行add All()函数)。
//无参
public LinkedList() {
}
//有参,构造一个包含指定集合元素的列表,这些元素将按照collection的迭代器返回的顺序排列。
public LinkedList(Collection<? extends E> c) {
this();//先构造一个空的列表
addAll(c);//将collection集合的数据添加到列表中
}
以下是addAll()方法 :
//addAll()
public boolean addAll(Collection<? extends E> c) {
//以collection为参数的构造传入size为0,即this()构造的空集合;
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);//判断下标是否合法
Object[] a = c.toArray();// 转array
int numNew = a.length;// 获取插入集合长度(元素个数)
if (numNew == 0) // 集合为空,返回false
return false;
Node<E> pred, succ;//前置节点、后置节点
if (index == size) {//在集合尾部添加数据(索引位置为队尾size)
succ = null;//后置节点为空
pred = last;//前置节点为数据最后一位
} else {
succ = node(index);//index索引位置节点作为后置节点
pred = succ.prev;index节点前一位即为前置节点
}
for (Object o : a) {//循环遍历集合数据
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);//以前置节点和元素e创建新节点
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;//添加成功
}
//判断索引是否合法部分代码:
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;//保证索引合法性
}
然后就是集合中常用的方法,我们就按增、删、改、查的顺序来说吧:
1、添加 : add(E e)(ps :addAll(Collection<? extends E> c)上面已经说过)
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++;//操作次数加一
}
2、删除 :remove(int index) / remove(Object o)
public E remove(int index) {
checkElementIndex(index);//校验索引合法性
return unlink(node(index));//删除索引位置节点
}
public boolean remove(Object o) {
if (o == null) {//对象为空
for (Node<E> x = first; x != null; x = x.next) {//循环集合
if (x.item == null) {//找到结果为null的节点元素
unlink(x);//执行删除
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {//找到集合内匹配o的元素
unlink(x);//执行删除
return true;
}
}
}
return false;//如果没有匹配数据,则返回false
}
删除操作执行的方法:实际原理为将传入的将要删除对象前后节点连接(跳过传入的对象),然后将删除节点位置的元素置为null;供垃圾回收器回收。
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) {// x的前置节点为空(即x为首节点)
first = next;// 将x的后置节点置为首节点
} else {
prev.next = next;//将x的后置节点赋值给x前置节点的后一节点(跳过x节点)
x.prev = null;//将x的前置节点置为null,即x不指向任何节点
}
if (next == null) { //如果x的后置节点为null(即x为最后一位)
last = prev;//将x的前一节点置为末节点
} else {
next.prev = prev;//将x的前置节点置为x后置节点的前一节点
x.next = null;//将x的后置节点置为null;即x不指向任何节点
}
x.item = null;将x的元素置为null
size--;//节点减一
modCount++;//操作次数加一
return element;//返回删除的元素
}
3、修改:set(int index, E element) 找到对应索引位置的节点,将节点的元素替换为传入元素
public E set(int index, E element) {
checkElementIndex(index);//判断索引合法性
Node<E> x = node(index);//找到索引对应的节点
E oldVal = x.item;//拿到节点的元素
x.item = element;//替换元素
return oldVal;//返回原来的元素
}
4、查询:get(int index) / getFirst() / getLast() 找到对应节点并获取节点下元素
public E get(int index) {//指定索引对应的节点
checkElementIndex(index);//判断索引合法性
return node(index).item;//获取节点元素
}
public E getFirst() {//获取第一个节点元素
final Node<E> f = first;
if (f == null)//如果集合为空则抛异常
throw new NoSuchElementException();
return f.item;//返回节点元素
}
public E getLast() {//获取最后节点元素
final Node<E> l = last;//集合为空抛异常
if (l == null)
throw new NoSuchElementException();
return l.item;//返回节点元素
}
5、迭代:listIterator(int index) ——从列表中的指定位置开始,返回此列表中元素的列表迭代器(按正确顺序)。
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);//检查索引合法性
return new ListItr(index);//返回迭代器的内部类
}
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);//判断索引是否为集合大小,如果是即下一个元素为null;否则下一个元素为索引对应的节点
nextIndex = index;
}
public boolean hasNext() {//判断是否有下一个
return nextIndex < size;//下一个索引位置小于集合大小
}
public E next() {
checkForComodification();//判断是否有并发操作,与ArrayList相似
if (!hasNext())//如果没有下一个,则抛异常
throw new NoSuchElementException();
lastReturned = next;//将下一个节点赋值给lastReturned
next = next.next;//将下一个节点的后置节点赋值给当前的后置节点(即继续向后遍历)
nextIndex++;//计数+1
return lastReturned.item;//返回lastReturned的元素
}
}
以上为LinkedList的源码分析内容,若有错误之处,欢迎评论指出,谢谢!
总结:LinkedList与ArrayList的相似之处为它们都是不同步的,如果在多线程下使用,可以使用这种方式处理:List list = Collections.synchronizedList(new LinkedList(...)),它们的不同之处是LinkedList是基于双链表结构,而ArrayList是基于数组结构,这也是他们在增删改查操作上有了效率上的差异,而且LinkedList内部除了存储了数据,同时还记录了其前后节点位置,因此LinkedList相对于ArrayList更消耗内存。
下期预告:HashMap源码学习(基于jdk8)。
传送门:
如果喜欢本文,请为我点赞,您的支持是我继续下去的动力,您也可以在评论区与我探讨或指正错误,最后别忘了关注一下我哦,谢谢。