在之前我们说到过一种特殊的Queue——PriorityQueue,但是我们最常用的Queue应该还要数LinkedList,本文就来看看它的内部实现。
我们可以看到,LinkedList类其实是List和Deque(双端队列)的结合,因此功能强大,受到大家的青睐。
1
LinkedList类本质上只包含四个属性:size表示当前列表长度,first表示头节点,last表示尾节点,以及继承自AbstractList的modcount属性记录对象结构变化次数。
首先看一下LinkedList类的构造函数,LinkedList一共有两个构造函数,其中一个是无参数的构造函数,创建了一个空列表;而另一个构造函数以集合为参数,先创建一个空列表,而后调用addAll
方法,将集合元素加入列表中。
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
LinkedList使用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;
}
}
我们可以看到Node类型除了自身属性值还有前后两个Node的引用,因此是个双向链表的结构。
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;
}
我们可以看到addAll
方法除了集合参数还有一个index参数,也就是说它支持将集合插入到现有的索引后面,参数index的值默认为LinkedList的size属性,也就是插入到末尾。方法首先检查了index值的合法性,如果index的值超过原链表的大小则会抛出异常,而后将集合转换为数组,并在原链表中寻找index对应的节点和它之前的节点。LinkedList类利用node方法来定位特定索引对应的Node
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;
}
}
可以看到如果对应索引在整个链表的额后半段则从后往前遍历,否则从前往后遍历,这也能体现出LinkedList类查找元素是比较耗费时间的。
找到插入点后就可以将集合中的元素依次链接起来,完成操作。这里要注意的是执行addAll
方法后,modCount会+1,不与集合的大小相关。
2
说完LinkedList是如何构造的,接下来讲讲LinkedList的操作函数。
LinkedList的增删基本上都由上面六个方法实现并进行封装后暴露出以下几个方法
public E removeFirst()//移除列表头,null抛出异常,实现Deque中接口
public E removeLast()//移除列表尾,null抛出异常,实现Deque中接口
public void addFirst(E e)//插入列表头,实现Deque中接口
public void addLast(E e)//插入列表尾,实现Deque中接口
public boolean add(E e) //插入列表尾,同addLast(E e),实现Collection中接口
public boolean remove(Object o)//从头遍历移除第一个出现的特定元素,实现Collection中接口
public boolean removeLastOccurrence(Object o)//从尾遍历移除第一个出现的特定元素,实现Deque中接口
public void add(int index, E element)//插入到列表特定位置,实现List中接口
public E remove(int index)//移除列表特定位置节点并返回,实现List中接口
public E poll()//移除列表头,null返回null,实现Queue中接口
public E pollFirst()//移除列表头,null返回null,同poll(),实现Deque中接口
public E pollLast()//移除列表尾,null返回null,实现Deque中接口
LinkedList类还对上述方法进行二次封装,输出以下方法
public E remove()//移除列表头,null抛出异常,同removeFirst(),实现Queue中接口
public boolean offer(E e)//插入列表尾,同add(E e),实现Queue中接口
public boolean offerFirst(E e)//插入列表头,返回true,同addFirst(E e),实现Deque中接口
public boolean offerLast(E e)//插入列表尾,返回true,同addLast(E e),实现Deque中接口
public void push(E e)//插入列表头,同addFirst(E e),实现Deque中接口
public E pop()//移除列表头,null抛出异常,同removeFirst(),实现Deque中接口
public boolean removeFirstOccurrence(Object o)//从头遍历移除第一个出现的特定元素,同remove(Object o),实现Deque中接口
这些方法没有什么特殊性,只是利用双向列表来操作对象,但是有一个clear
方法需要注意下
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
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++;
}
由于LinkedList是链表形式的,因此在调用clear
时不仅要将存储的元素置空,还要将其前后的关联断开。
LinkedList的查询主要依靠以下几个函数来实现
Node<E> node(int index)//根据索引来从前或从后遍历获取并返回节点,index非法会返回null
public int indexOf(Object o)//从前向后遍历查找返回索引,未找到返回-1
public int lastIndexOf(Object o)//从后向前遍历查找返回索引,未找到返回-1
public E getFirst()//返回first属性,若为空则抛异常
public E getLast()//返回last属性,若为空则抛异常
public E peek()//返回first属性,若为空则返回null
public E peekFirst()//返回first属性,若为空则返回null,同peek()
public E peekLast()//返回last属性,若为空则返回null
public E get(int index)//根据索引来从前或从后遍历获取并返回元素,index非法会抛异常
LinkedList的修改只基于一个函数
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
首先检查索引是否非法,而后赋值,比较特殊的是它会将原值返回出来。
3
说完增删改查,我们最后来看看LinkedList的迭代器实现。
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);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
首先我们看到LinkedList迭代器的构造函数有一个参数index,也就是说LinkedList支持生成特定位置开始的迭代器。构造函数会根据index找到对应的Node给next属性赋值并将nextIndex属性赋值为index,另外迭代器初始化时会将expectedModCount赋值为LinkedList的modCount值。
迭代器在每次循环之前会调用hasNext
函数来判断集合是否还有下一个元素,然后调用next
方法将元素取出。与其他的集合类相差不大,比较特殊的是LinkedList还提供了一个反向的迭代器DescendingIterator
private class DescendingIterator implements Iterator<E> {
private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}
这个迭代器用hasPrevious
函数代替了hasNext
函数,previous
函数代替了next
函数,而且起始于列表尾部。
4
最后我们来看LinkedList中另一个迭代器LLSpliterator,这是一个专为多线程并行迭代准备的一个迭代器。
我们可以调用spliterator
函数来获取一个Spliterator迭代器
@Override
public Spliterator<E> spliterator() {
return new LLSpliterator<E>(this, -1, 0);
}
可以看到初始化了一个LLSpliterator对象,那我们继续看LLSpliterator的属性和构造函数。
static final int BATCH_UNIT = 1 << 10; // batch array size increment
static final int MAX_BATCH = 1 << 25; // max batch array size;
final LinkedList<E> list; // null OK unless traversed
Node<E> current; // current node; null until initialized
int est; // size estimate; -1 until first needed
int expectedModCount; // initialized when est set
int batch; // batch size for splits
LLSpliterator(LinkedList<E> list, int est, int expectedModCount) {
this.list = list;
this.est = est;
this.expectedModCount = expectedModCount;
}
我们可以看到我们可以获取到一个est属性为-1,expectedModCount属性为0的LLSpliterator对象。
通常我们使用Spliterator迭代器会多个线程分别调用Spliterator对象的trySplit
方法来获取一个拆分好的迭代器,然后调用forEachRemaining方法来并行迭代。
public Spliterator<E> trySplit() {
Node<E> p;
int s = getEst();
if (s > 1 && (p = current) != null) {
int n = batch + BATCH_UNIT;
if (n > s)
n = s;
//此处感觉永远不会满足if条件
if (n > MAX_BATCH)
n = MAX_BATCH;
Object[] a = new Object[n];
int j = 0;
do { a[j++] = p.item; } while ((p = p.next) != null && j < n);
current = p;
batch = j;
est = s - j;
return Spliterators.spliterator(a, 0, j, Spliterator.ORDERED);
}
return null;
}
final int getEst() {
int s; // force initialization
final LinkedList<E> lst;
if ((s = est) < 0) {
if ((lst = list) == null)
s = est = 0;
else {
expectedModCount = lst.modCount;
current = lst.first;
s = est = lst.size;
}
}
return s;
}
我们看到第一次调用trySplit
方法时,会将est属性的值变更为LinkedList对象的大小,current属性指向LinkedList对象的头节点,expectedModCount属性赋值为LinkedList对象的modCount属性。
可以看到当前Spliterator的最大遍历元素个数限制为batch + BATCH_UNIT,初始为1024,所以每次调用完trySplit
方法都会将batch的值设置为当前Spliterator需遍历元素个数,因此Spliterator依次最大限制为1024,2048,3072,…,…,直到所有元素都分配完或者达到最大上限MAX_BATCH,当所有元素都分配好之后如果再调用trySplit
方法将会得到null。
我们用下面的代码来测试
public class Atest {
List<Integer> strList = createList();
Spliterator spliterator = strList.spliterator();
@Test
public void mytest(){
for(int i=0;i<4;i++){
new MyThread().start();
}
try {
Thread.sleep(1500000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
class MyThread extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("线程"+threadName+"开始运行-----");
Spliterator sp = spliterator.trySplit();
sp.forEachRemaining(new Consumer() {
@Override
public void accept(Object o) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数值:"+o+"------"+threadName);
}
});
System.out.println("线程"+threadName+"运行结束-----");
}
}
private List<Integer> createList(){
List<Integer> result = new LinkedList<>();
for(int i=0; i<10000; i++){
result.add(i);
}
return result;
}
}
可以看到运行结果为
这也可以印证之前源码的解释。
在上面的例子中,我们使用forEachRemaining
函数对每一个Spliterator进行遍历,也就是会按顺序对每一个元素执行我们定义的Consumer类的accept
方法,另外还有一个方法tryAdvance
仅会对Spliterator中第一个元素执行accept
方法,因此我们上面的代码也可以在外面循环,更加灵活一些
class MyThread extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("线程"+threadName+"开始运行-----");
Spliterator sp = spliterator.trySplit();
Consumer consumer = o -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数值:"+o+"------"+threadName);
};
while (sp.tryAdvance(consumer)){
\\某些处理
}
System.out.println("线程"+threadName+"运行结束-----");
}
}
不过特别要注意的是,使用Spliterator来遍历一定要重复获取Spliterator对象直到trySplit
函数返回null,否则有元素遗漏的可能。
本文只要介绍了LinkedList类型的数据结构和主要方法以及迭代器相关的要点,希望你看了能有一点收获。