本文部分内容来自:Java集合详解2:LinkedList和Queue,大部分内容为个人原创。
本文主要通过源码分析 LinkedList 和 Queue 这两种集合。
本文目录:
1、LinkedList
- LinkedList 的类结构
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;
// 自定义迭代器 next
private class ListItr implements ListIterator<E> {
//...
private int expectedModCount = 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;
}
}
// 从后往前遍历的迭代器 previous
private class DescendingIterator implements Iterator<E> {
// ...
}
}
- LinkedList 的构造函数
// 构造函数1
public LinkedList() {
}
// 构造函数2
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
1.1 概述
LinkedList 与 ArrayList 一样实现 List 接口,只是 ArrayList 是 List 接口的大小可变数组的实现,LinkedList 是 List 接口链表的实现。基于链表实现的方式使得 LinkedList 在插入和删除时更优于 ArrayList,而随机访问则比 ArrayList 逊色些。
LinkedList 实现 List 接口所有可选的列表操作,并允许所有的元素包括 null。
除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。
此类还实现了 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。
所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。
同时,与ArrayList一样此实现不是同步的。
- LinkedList 的定义
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
}
从这段代码中我们可以清晰地看出 LinkedList 继承了 AbstractSequentialList 类,实现了 List、Deque、Cloneable、Serializable 接口。其中 AbstractSequentialList 提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,从而以减少实现List接口的复杂度。Deque 是一个线性的 Collection,支持在两端插入和移除元素,定义了双端队列的操作。
- LinkedList 的属性
在LinkedList中提供了三个基本属性 size、first、last。
// 集合大小
transient int size = 0;
// 头指针
transient Node<E> first;
// 尾指针
transient Node<E> last;
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;
}
}
1.2 增删改查
- 增加方法
LinkedList 中的添加操作很多,常用的几个方法如下:
方法1:add(E, e) 和 addLast:将指定元素添加到此链表的末尾处。
// 添加操作
public boolean add(E e) {
linkLast(e);
return true;
}
// 添加到尾节点
public void addLast(E e) {
linkLast(e);
}
// 往链表的末尾添加一个元素
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
// 将新节点设置为尾节点
last = newNode;
if (l == null)
// 如果原链表为空,则将头节点也设置成 newNode
first = newNode;
else
// 否则将新结点设置为原尾节点的后继节点
l.next = newNode;
// 链表大小加1
size++;
// 链表的修改次数加1
modCount++;
}
方法2:addFirst:将新元素添加到首节点处:
// 添加到首节点
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;
// 集合元素大小加1
size++;
// 集合的修改次数加1
modCount++;
}
方法3:插入指定的位置处:add(int index, E element)
// 插入指定的位置处
public void add(int index, E element) {
// 检查下标
checkPositionIndex(index);
if (index == size)
// 如果 index 为当前链表尾节点,那么直接将新节点插入到尾节点处
linkLast(element);
else
// 其实是插入到 index 位置处节点的前面
linkBefore(element, node(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;
}
}
// 将新节点插入到succ节点的前面
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);
// 将新节点插入到index位置节点的前面
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
方法4:一次添加多个节点,不再详解。
public boolean addAll(int index, Collection<? extends E> c) { // ... }
public boolean addAll(Collection<? extends E> c) { // ...}
- 移除方法
方法1:remove(Object o):移除链表中指定的元素
public boolean remove(Object o) {
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;
}
该方法首先会判断移除的元素是否为null,然后迭代这个链表找到该元素节点,最后调用 unlink(Node<E> x),它是LinkedList 中所有移除方法的基础方法,如下:
E unlink(Node<E> x) {
// 这个方法中的x不能为 null
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 判断x节点是否为头节点
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 判断x节点是否为尾节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// 将x节点的值设置为null
x.item = null;
// 集合元素大小减1
size--;
// 修改集合的次数加1
modCount++;
return element;
}
其他移除方法如下:
clear(): 从此列表中移除所有元素。
remove():获取并移除此列表的头(第一个元素)。
remove(int index):移除此列表中指定位置处的元素。
remove(Objec o):从此列表中移除首次出现的指定元素(如果存在)。
removeFirst():移除并返回此列表的第一个元素。
removeFirstOccurrence(Object o):从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
removeLast():移除并返回此列表的最后一个元素。
removeLastOccurrence(Object o):从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。
- 查找方法
对于查找方法的源码就没有什么好介绍了,无非就是迭代,比对,然后就是返回当前值。
get(int index):返回此列表中指定位置处的元素。
getFirst():返回此列表的第一个元素。
getLast():返回此列表的最后一个元素。
indexOf(Object o):返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。
lastIndexOf(Object o):返回此列表中最后出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。
- 修改方法
public E set(int index, E element) {
// 检查index
checkElementIndex(index);
// 找出index处的节点
Node<E> x = node(index);
// 更新值
E oldVal = x.item;
x.item = element;
// 返回旧值
return oldVal;
}
2、Queue
- Queue 接口的源码
public interface Queue<E> extends Collection<E> {
//添加元素
boolean add(E e);
// 添加元素
boolean offer(E e);
// 移除元素,队列为空时会抛出异常
E remove();
// 移除元素,队列为空时会返回null
E poll();
// 获取元素,队列为空时会抛出异常
E element();
// 获取元素,队列为空时会返回null
E peek();
}
Queue接口定义了队列数据结构,元素是有序的(按插入顺序),先进先出。Queue接口相关的部分UML类图如下:
2.1、DeQueue 接口
package java.util;
public interface Deque<E> extends Queue<E> {
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getFirst();
E getLast();
E peekFirst();
E peekLast();
boolean removeFirstOccurrence(Object o);
boolean removeLastOccurrence(Object o);
// *** Queue methods ***
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
// *** Stack methods ***
void push(E e);
E pop();
// *** Collection methods ***
boolean remove(Object o);
boolean contains(Object o);
public int size();
Iterator<E> iterator();
Iterator<E> descendingIterator();
}
DeQueue(Double-ended queue)为接口,继承了Queue接口,创建双向队列,灵活性更强,可以前向或后向迭代,在队头队尾均可随心插入或删除元素。它的两个主要实现类是 ArrayDeque 和 LinkedList。
LinkedList 上面已经讲过了,下面就讲下 DeQueue 的另外一个实现类:ArrayDeque。
2.2 ArrayDeque 实现类
ArrayDeque 底层使用的是循环数组实现 “双端 / 双向队列”。
- ArrayDeque 的类结构
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable{
// 元素都保存在数组中
private transient E[] elements;
// 头索引下标
private transient int head;
// 尾索引下标
private transient int tail;
// 最小容量
private static final int MIN_INITIAL_CAPACITY = 8;
// 自定义迭代器:从前往后遍历
private class DeqIterator implements Iterator<E> {
private int cursor = head;
private int fence = tail;
// ...
}
// 自定义迭代器:从后往前遍历
private class DescendingIterator implements Iterator<E> {
private int cursor = tail;
private int fence = head;
// ...
}
// ...
}
- ArrayDeque 的类构造器
// 构造器1:默认初始化大小为16
public ArrayDeque() {
elements = (E[]) new Object[16];
}
// 构造器2:指定初始化大小
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
// 构造器3:根据集合c指定大小
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
可以看到除了默认构造器外,其他两种指定大小的构造器都是调用 allocateElements(int numElements) 方法完成的:
private void allocateElements(int numElements) {
// 最小容量为8
int initialCapacity = MIN_INITIAL_CAPACITY;
// 如果要分配的容量大于等于8,扩大成2的幂(是为了维护头、尾下标值);
// 否则使用最小容量8
if (numElements >= initialCapacity) {
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
elements = (E[]) new Object[initialCapacity];
}
可以看到容量的大小都是2的幂,这样是为了方便维护头、尾的下标。
- 添加操作
ArrayDeque 的添加方法很多,具体的方法看上面的类结构所示。这里就简单分析下常用的几种添加元素的方法:
方法1:add(E e) 方法:实际上调用的是addLast(E e)方法,即将元素添加到数组的结尾处
public boolean add(E e) {
// 调用addLast方法
addLast(e);
return true;
}
那下面就来看一看 addLast 方法:
public void addLast(E e) {
if (e == null)
// 如果e为空指针则抛出空指针异常
throw new NullPointerException();
// 将新元素放在tail下标处
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
// 扩容
doubleCapacity();
}
这里看下 addLast 方法中的扩容时机:
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
// 扩容
doubleCapacity();
尾索引+1 然后与数组的长度减1(elements - 1)进行“与运算”。因为 length 是2的幂,所以 (length - 1)转换为二进制后全是1。
所以如果尾索引值 tail 小于等于 (length - 1),那么“与运算”后仍为 tail 本身,如果刚好比(length - 1)大 1 时(即其二进制除了首位,其他位都是0),”与运算“后 tail 便为 0(即回到了初始位置)。
正是通过 (tail + 1) & (elements.length - 1) 这个“与运算”实现了数组的双向循环。
如果尾索引和头索引重合了,则说明数组已满,需要进行扩容,扩容为以前的2倍。
private void doubleCapacity() {
// 先判断头节点是否和尾节点重合了
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // number of elements to the right of p
// 向左移动一位,即扩大一倍
int newCapacity = n << 1;
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r);
System.arraycopy(elements, 0, a, r, p);
elements = (E[])a;
head = 0;
tail = n;
}
方法2:addFirst(E e) 的实现
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
/** 此处如果head为0,则-1(1111 1111 1111 1111 1111 1111 1111 1111)
与(length - 1)进行取‘&’运算,结果必然是(length - 1),
即回到了数组的尾部。 */
elements[head = (head - 1) & (elements.length - 1)] = e;
// 如果尾索引和头索引重合了,说明数组满了,进行扩容
if (head == tail)
doubleCapacity();
}
- 删除操作
remove() 相关的方法实际上调用的都是 poll() 相关方法去完成的:
public E remove() {
// 调用 removeFirst方法
return removeFirst();
}
public E removeFirst() {
// 调用 pollFirst 方法
E x = pollFirst();
if (x == null)
throw new NoSuchElementException();
return x;
}
public E pollFirst() {
// 将队首元素弹出
int h = head;
E result = elements[h]; // Element is null if deque empty
if (result == null)
return null;
elements[h] = null; // Must null out slot
// 头索引加1
head = (h + 1) & (elements.length - 1);
return result;
}
除此之外,还有以下删除方法:
// 移除队尾元素
public E removeLast() {
E x = pollLast();
if (x == null)
throw new NoSuchElementException();
return x;
}
public E pollLast() {
// 尾索引-1
int t = (tail - 1) & (elements.length - 1);
// 删除队尾元素
E result = elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}
- 查找操作
// 获取元素:实际上调用的是getFirst()方法
public E element() {
return getFirst();
}
// 获取队首元素
public E getFirst() {
E x = elements[head];
if (x == null)
throw new NoSuchElementException();
return x;
}
// 获取队尾元素
public E getLast() {
E x = elements[(tail - 1) & (elements.length - 1)];
if (x == null)
throw new NoSuchElementException();
return x;
}
2.3 PriorityQueue 实现类
PriorityQueue 底层是用数组实现堆的数据结构。
- PriorityQueue 的类结构
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
// 默认的初始化大小是11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 数据结构为:数组
private transient Object[] queue;
// 集合元素的大小
private int size = 0;
// 因为要用数组实现堆结构,所以需要使用 Comparator
private final Comparator<? super E> comparator;
// 记录 PriorityQueue 的修改次数
private transient int modCount = 0;
// 自定义迭代器
private final class Itr implements Iterator<E> { // ...}
// ... 省略
}
- PriorityQueue 的构造函数
PriorityQueue 一共有6个构造器,只需要关注前三个就行了。
// 构造函数1:默认构造器
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
// 构造器2:自定义大小
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
// 构造器3:实现堆结构
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
// 构造器4
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
else {
this.comparator = null;
initFromCollection(c);
}
}
// 构造器5:
@SuppressWarnings("unchecked")
public PriorityQueue(PriorityQueue<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initFromPriorityQueue(c);
}
// 构造器6:
@SuppressWarnings("unchecked")
public PriorityQueue(SortedSet<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initElementsFromCollection(c);
}
优先队列跟普通的队列不一样,普通队列是一种遵循 FIFO 规则的队列,拿数据的时候按照加入队列的顺序拿取。 而优先队列每次拿数据的时候都会拿出优先级最高的数据。
优先队列内部维护着一个堆,每次取数据的时候都从堆顶拿数据(堆顶的优先级最高),这就是优先队列的原理。
- 添加方法 / 入队方法
调整方法需要关注下,因为往堆里插入一个元素,是先插入到叶子节点,再从下往上进行调整,要理解这个过程的实现。
首先调用 add(E e) 方法:
public boolean add(E e) {
// 调用 offer 方法
return offer(e);
}
调用 offer(E e) 方法:
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
// 集合操作次数加1
modCount++;
int i = size;
// 如果当前用堆表示的数组已经满了,调用grow方法扩容
if (i >= queue.length)
grow(i + 1);
size = i + 1;
// 如果堆为空的时候
if (i == 0)
// 直接给堆顶元素赋值
queue[0] = e;
else
// 堆中已有元素的情况
// 重新调整堆,从下往上调整,因为新增元素是先加到最后一个叶子节点
siftUp(i, e);
return true;
}
下面就看看添加元素后,调整堆所用到的 siftUp(int k , E x) 的内部实现:
private void siftUp(int k, E x) {
// 判断比较器是否存在
if (comparator != null)
// 使用比较器调整
siftUpUsingComparator(k, x);
else
// 比较器不存在的情况下,使用元素自身的比较进行调整
siftUpComparable(k, x);
}
再看看存在比较器时,调用的 siftUsingComparator(int k, E x) 方法的实现:
private void siftUpUsingComparator(int k, E x) {
// 一直循环直到父节点还存在
while (k > 0) {
// 找到父节点索引,等同于(k - 1)/ 2
int parent = (k - 1) >>> 1;
// 获得父节点元素
Object e = queue[parent];
// 新元素与父元素进行比较,如果满足比较器结果,直接跳出,否则进行调整
if (comparator.compare(x, (E) e) >= 0)
break;
// 进行调整,新位置的元素变成了父元素
queue[k] = e;
// 新位置索引变成父元素索引,进行递归操作
k = parent;
}
// 新添加的元素添加到堆中
queue[k] = x;
}
- 出队方法:poll
删除元素:删除的是堆顶元素,删除后需要从上往下调整堆结构。
public E poll() {
if (size == 0)
return null;
// 元素个数减1
int s = --size;
modCount++;
// 得到堆顶元素
E result = (E) queue[0];
// 最后一个叶子节点
E x = (E) queue[s];
// 将最后一个叶子节点设置为空
queue[s] = null;
if (s != 0)
// 从上往下调整,因为删除元素,删除的是堆顶元素
siftDown(0, x);
return result;
}
下面就看下,从上往下调整的方法:siftDown(int k, E x) 的内部实现:
private void siftDown(int k, E x) {
// 比较器存在的情况下
if (comparator != null)
// 使用比较器调整
siftDownUsingComparator(k, x);
else
// 比较器不存在的情况下,使用元素自身的比较调整
siftDownComparable(k, x);
}
可以看到其内部调用了比较器方法:siftDownUsingComparator(int k, E x) :
private void siftDownUsingComparator(int k, E x) {
// 只需循环节点个数的一半即可
int half = size >>> 1;
while (k < half) {
// 得到父节点的左子节点索引,即(k * 2)+ 1
int child = (k << 1) + 1;
// 得到左子元素
Object c = queue[child];
// 得到父节点的右子节点索引
int right = child + 1;
// 左子节点跟右子节点比较,取更大的值
if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
// 然后这个更大的值跟最后一个叶子节点比较
if (comparator.compare(x, (E) c) <= 0)
break;
// 新位置使用更大的值
queue[k] = c;
// 新位置索引变成子元素索引,进行递归操作
k = child;
}
// 最后一个叶子节点添加到合适的位置
queue[k] = x;
}
- 删除队列元素:remove
public boolean remove(Object o) {
// 找到 o 在队列中对应的索引
int i = indexOf(o);
// 不存在的话直接返回 false
if (i == -1)
return false;
else {
// 存在的话调用 removeAt 方法,并返回true
removeAt(i);
return true;
}
}
那么下面就看下根据下标删除元素的方法:remove(int i) 的内部实现:
private E removeAt(int i) {
// 保证i的有效性
assert i >= 0 && i < size;
modCount++;
// 元素个数减1
int s = --size;
// 如果是删除最后一个叶子节点
if (s == i)
// 直接置空,删除即可,堆还是保持特质,不需要调整
queue[i] = null;
// 如果是删除的不是最后一个叶子节点
else {
// 获得最后1个叶子节点元素
E moved = (E) queue[s];
// 将最后1个叶子节点置空
queue[s] = null;
// 从上往下调整
siftDown(i, moved);
// 如果从上往下调整完毕之后发现元素位置没变,再从下往上调整
if (queue[i] == moved) {
// 从下往上调整
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
先执行 siftDown() 下滤的过程:
再执行 siftUp() 的过程:
2.4 总结和同步的问题
1、JDK 内置的优先队列PriorityQueue内部使用一个堆维护数据,每当有数据add进来或者poll出去的时候会对堆做从下往上的调整和从上往下的调整。
2、PriorityQueue不是一个线程安全的类,如果要在多线程环境下使用,可以使用 PriorityBlockingQueue 这个优先阻塞队列。其中add、poll、remove方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞。