LinkedList详解
1.LinkedList是什么?
从图中可以看出来,LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作,同时它也实现 List 接口,所以能对它进行队列操作,并且它也实现了 Deque 接口,为 add、poll 提供先进先出队列操作,从而能将LinkedList当作双端队列使用。它还实现了Cloneable接口,Serializable接口,所以它支持克隆和序列化,但是LinkedList是一个非线程安全的List.
重要参数
transient Node first:
双向链表的表头,它是双向链表节点所对应的类Node的实例。Node中包含成员变量:prev, next, item。
transient Node last:
双向链表的表尾。
transient int size:
双向链表中节点的个数。
Node
Node节点一共有三个属性:item代表节点值,prev代表节点的前一个节点,next代表节点的后一个节点。每个结点都有一个前驱和后继结点,并且在 LinkedList中也定义了两个变量分别指向链表中的第一个和最后一个结点。
1.LinkedList怎么实现的?
老样子,还是先从构造方法开始看:
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
前一个构造方法为空,里面不含任何元素。后者构造一个包含指定 collection 中的元素的列表。构造函数首先会调用LinkedList(),构造一个空列表,然后调用了addAll()方法将Collection中的所有元素添加到列表中,再来看addAll()和一张数据结构图:
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);//检查下标,若插入的位置小于0或者大于链表长度,则抛出IndexOutOfBoundsException异常
Object[] a = c.toArray();
int numNew = a.length;//插入元素个数
if (numNew == 0)
return false;
Node<E> pred, succ; //定义perv与next
if (index == size) { //如果在队尾插入
succ = null; //next置空
pred = last; //perv指向队尾元素last
} else { //在指定位置插入
succ = node(index); //next指向该位置
pred = succ.prev; //perv指向前一个元素
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);//创建一个新节点,指定perv,next置空
if (pred == null)//如果perv不存在
first = newNode;//表头first指向此节点
else
pred.next = newNode;//perv存在,则将其next指向新节点
pred = newNode;//perv移动,继续创建新节点
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
LinkedList是通过双向链表去实现的。既然是双向链表,那么它的顺序访问会非常高效,而随机访问效率比较低;它也实现了List接口{也就是说,它实现了get(int location)、remove(int location)等根据索引值来获取、删除节点的函数.那么既然它实现了List接口,又是如何将双向链表和索引值联系起来的呢?其实它就是通过一个计数索引值来实现的。例如,当我们调用get(int index),set、add、remove时,首先会比较index和双向链表长度的1/2;若前者大,则从链表头开始往后查找,直到index位置;否则,从链表末尾开始先前查找,直到index位置,从而节省一半的查找时间.当然它还提供了一些辅助方法主要是用来添加和删除元素用的.这里要说一个遍历问题,简直采用迭代器或者for循环去遍历LinkedList,不建议采用随机访问的方式,不然效率太低下.
方法 | 作用 |
---|---|
linkFirs | 插入头部 |
linkLast | 插入尾部 |
linkBefore | 插入到某个节点前 |
unlinkFirst | 删除头部 |
unlinkLast | 删除尾部 |
unlink | 删除某节点 |
size | 获取长度 |
isEmpty | 是否为空 |
contains | 是否包含 |
说到LinkedList的遍历就要说一件值得关注的事,大家都知道,java集合(Collection)中有一种错误机制是fail-fast 机制,也就是用来错误检查的机制,简单来说就是当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;就会抛出ConcurrentModificationException异常,产生fail-fast事件.所以对于我们经常要考虑线程安全的问题,还是必要的时候采用java.util.concurrent包下对应的类来代替它比较好,或者采用Collections工具类中的同步方法去解决,当然ArrayList也是一样的.
那fail-fast是怎么来的?其实原理很简单,在AbstractList里定义了一个叫modCount的变量
protected transient int modCount = 0;
它存在的意义就是在有其他操作对List进行修改时,自动加1;例如在ArrayList里的 ensureExplicitCapacity 方法,remove方法,clear方法等等。产生fail-fast事件,是通过抛出ConcurrentModificationException异常来触发的。而ConcurrentModificationException是在操作Iterator时抛出的异常。如果去查看Iterator的源码你会发现,Iterator里定义了一个叫expectedModCount的变量,初始化等于modCount的值。所以每次遍历List中的元素的时候,都会比较 expectedModCount 和 modCount 是否相等。如果不相等则抛出异常。
那么什么时候 modCount 不等于 expectedModCount呢?查看ArrayList的源码,如上面所说,无论是 ensureExplicitCapacity()、add()、remove(),还是clear(),只要涉及到修改集合中的元素个数时,都会改变modCount的值。总结一下就是当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
解决原理
查看和ArrayList对应的CopyOnWriteArrayList的源码。举个最简单的例子add方法:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray(); //copy一份原来的array
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e; //在copy的数组上add
setArray(newElements); //原有引用指向修改后的数据
return true;
} finally {
lock.unlock();
}
}
CopyOnWriteArrayList的add、set、remove等会改变原数组的方法中,都是先copy一份原来的array,再在copy数组上进行add、set、remove操作。然后把原有数据的引用改成指向修改后的数据,这就才不影响COWIterator那份数组。
1.LinkedList的优缺点
ArrayList的数据结构为线性表,而LinkedList数据结构是双向链表。链表数据结构的特点是每个元素分配的空间不必连续、插入和删除元素时速度非常快、但访问元素的速度较慢,当数据量很大或者操作很频繁的情况下,添加和删除元素时具有比ArrayList更好的性能,但在元素的查询和修改方面要弱于ArrayList,LinkedList类每个结点用内部类Node表示,LinkedList通过first和last引用分别指向链表的第一个和最后一个元素,当链表为空时,first和last都为NULL值.
总结一下:
优点:
插入和删除元素效率高
缺点:
访问元素的速度和修改元素的速度比较慢,顺序访问速度搞,随机访问效率低
是非线程安全的