ArrayList和LinkedList以及Vector都是基础容器List中的重要成员,List集合与map,set的区别是,List是以线性方式存储的,有序的(先进先出),可重复的容器,可以设置多个null值。而set是无序不重复的容器,Map没有继承Collection,是有序不重复的。今天我们来了解一下LinkedList。
LinkedList
一. LinkedList的继承关系
我们知道ArrayList的父类是AbstractList,并且实现了List接口。根据上图可以看到,LinkedList继承自AbstractSequentialList,而AbstractSequentialList则继承了AbstractList。除此之外,LinkedList还实现了List接口和Qeque接口。我们接下来进入解析源码过程。
1.AbstractSequentialList
查看源码可以得知,该抽象类包含一个无参构造方法,以及以下几个方法:
1.1get(int index)
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
该方法通过调用内部listIterator方法创建了一个ListIterator迭代器,再获取,根据index值获取对应角标的数据。而该方法需要子类实现。需要注意的是,ListIterator是一个接口,与Iterator不同,它只能用于List中,每个List需要实现自己的ListIterator接口,作为一个私有的内部类存在List中,如:ArrayList和LinkedList均实现了这个内部类。
1.2.set(int index,E element)
public E set(int index, E element) {
try {
ListIterator<E> e = listIterator(index);
E oldVal = e.next();
e.set(element);
return oldVal;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
get方法调用内部方法获取迭代器,将迭代器中指定角标的位置修改,再返回原数据
剩下的方法不再赘述,都是围绕迭代器进行的,包括添加数据,移除数据等等。
总结:AbstractSequentialList抽象类定义了链表结构list集合的基础方法,同时其子类需要实现listIterator和iterator方法。
2.Queue
队列是一种特殊的线性表,它定义了队列的基本操作方法,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。
LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用,LinkedList之所以比ArrayList插入和新增效率高,是因为ArrayList每次插入数据,需要进行大量的移动操作,甚至需要进行扩容。而linkedList则避免了这些问题。我们后面会聊到。
3.Deque
继承自Queue,在队列的基础上扩展,新增了较多方法定义,例如:poll()、peek()等等。
4.LinkedList
//无参构造函数
public LinkedList() {
}
//有参构造函数
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList有两个构造函数,当没有传入参数时,只创建LinkedList对象,当我们将一个Collection当作参数传入时,先调用无参构造函数,再将集合中数据插入。
public boolean add(E e) {
linkLast(e);
return true;
}
创建LinkedList集合后,要往链表中添加数据,我们需要用到add方法,add方法在内部调用了linkLast方法,从方法名可以看出,LinkedList的add方法默认是添加到链表末尾。我们跟踪进去看看。
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;//正常的在最后一个节点后追加,那么原先的最后一个节点的next就要指向现在真正的最后一个节点,原先的最后一个节点就变成了倒数第二个节点
size++;
modCount++;
}
//创建节点
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;
}
}
linkLast方法执行流程是:
1.创建一个名1为l的final的Node指向链表最后一个节点,然后创建一个新节点,将需要添加的数据传入Node的构造函数中。
2.newNode 变量为新创建的Node节点,此时该节点的prvious为last的值,next的值为空。
3.让最后一个节点指向新的节点
4.判断l是否,链表当前为空,则第一个节点和最后一个节点都是newNode,第一个和最后一个节点都指向newNode ,也就是说newNode 是第一个节点。
5.链表长度+1,修改次数+1
这里我们可以分析看出LinkedList的存储方式如下图:
我们再分析Linked的特点:
1.ArrayList是有序的,LinkedList是无序的,这个怎么理解呢?
ArrayList的底层数据结构是数组,我们知道数组在创建时长度就是固定的,JVM分配的空间地址也是确定的、连续的,因此数组每个元素之间是有序的。而linkedList,我们在上面的示例中可以看出:链表中的每一个节点之间都是系统单独分配的内存空间,每一个Node对象它们的内存地址也是没有规律的,它们通过保存前后节点相关联。
2.性能方面的优劣
ArrayList由于内存地址是连续的,我们可以推出,当需要对List进行查询,筛选时,用ArrayList合适,而如果需要进行大量的修改,用linkedList比较合适,因为ArrayList因为内存空间是连续的,每次进行修改,需要移动的数据非常大,若空间不足,还需要进行扩容,复制数据。操作尤为繁琐。相反LinkedList,由于每个元素空间不连续,进行插入或修改时,只需要将前后节点的指向进行修改,所以效率较高。相比较修改插入数据,查询数据则是LinkedList的弱势了,由于空间不连续,所以只能从链表开头或结尾查找,直到匹配上。
4.LinkedList的addAll()方法
//在集合末尾添加一个集合
public boolean addAll(Collection<? extends E> c) {
//当前链表长度为size
return addAll(size, c);
}
//将集合添加到指定角标之后
public boolean addAll(int index, Collection<? extends E> c) {
//检查是否存在index,这是通过size判断的,如果index<0或index>size,则抛出角标越界异常。
checkPositionIndex(index);
//将集合转换成数组进行查找
Object[] a = c.toArray();
int numNew = a.length;
//若需要添加的集合为空,添加失败
if (numNew == 0)
return false;
Node<E> pred, succ;
//index==size满足,则说明是加到链表末尾
if (index == size) {//加到链表末尾
succ = null;
//当前节点的前一个节点是原本最后一个节点
pred = last;
} else {
//不是加到链表结尾
succ = node(index);
//如果不是链表结尾,则设置当前节点的前一个节点为指定角标的后面一个节点
pred = succ.prev;
}
//开始遍历集合,一个一个添加
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//新建一个节点,该节点的值为o,该节点的前一个值为pred
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
//若当前链表为空,则最后一个节点与第一个节点都为newNode
first = newNode;
else
//设置原本最后一个节点的后一个节点为newNode
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
5.LinkedList的indexOf方法
public int indexOf(Object o) {
int index = 0;
//若当前对象为空
if (o == null) {
//从第一个节点开始找,通过x.next()遍历整个List,进行对比
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;
}
6.lastIndexOf()
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
//从最后一个查找,与indexOf相似。
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;
}
7.poll()从链表最前面删除一个节点
public E poll() {
final Node<E> f = first;
//判断如果第一个元素不为空,则说明链表中包含元素,调用unlinkFirst删除
return (f == null) ? null : unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//获取第一个元素
final E element = f.item;
//取出第一个节点的后一个元素
final Node<E> next = f.next;
//将第一个节点置空
f.item = null;
f.next = null; // help GC
//first节点指向后一个节点
first = next;
//链表为空
if (next == null)
//当前节点的后一个节点置空
last = null;
else
//否则将第一个节点置空
next.prev = null;
size--;
modCount++;
return element;
}