Queue(队列)是拥有 先进先出 (FIFO)特性的数据结构,PriorityQueue(优先级队列)是它的子类之一,不同于先进先出,它可以通过比较器控制元素的输出顺序(优先级)。本文就来分析一下PriorityQueuede的源码,看看它是如何实现的。
先来看 Queue
接口:
public interface Queue<E> extends Collection<E> 复制代码
Queue接口继承了Collection接口,表示集合。它提供了三种方法,即:增加、删除、获取,每种都有两个实现。
// 增加元素 boolean add(E e); boolean offer(E e); // 删除元素 E remove(); E poll(); // 获取元素 E element(); E peek(); 复制代码
两种实现的区别是, add
、 remove
和 element
都比对应的 offer
、 poll
和 peek
多抛出一个异常。对于 add
方法,如果因为容量不足以增加新元素,会抛出 IllegalStateException
异常,而 offer
则会返回false。对于 remove
方法,如果队列是空的,则会抛出 NoSuchElementException
异常,而 poll
方法会返回null。对于 element
方法,如果队列是空的,则会抛出 NoSuchElementException
异常,而 peek
方法会返回null。
再看 PriorityQueue
类:
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable 复制代码
嗯,不是直接实现 Queue
,而是继承了 AbstractQueue
类。又是熟悉的感觉, AbstractQueue
应该也是模板抽象类(和AbstractMap和AbstractList类似)。
来看 AbstractQueue
:
public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> 复制代码
果然,继承了 AbstractCollection
类和实现了 Queue
接口。既然是模板类那肯定有模板方法。 AbstractQueue
源码中只实现了 add
、 remove
和 elemet
方法。
public boolean add(E e) { if (offer(e)) // 调用offer ... } public E remove() { E x = poll(); // 调用poll ... } public E element() { E x = peek(); // 调用peek ... } 复制代码
可以看到,它们分别调用了 offer
、 poll
和 peek
。也就是说,如果要通过 AbstractQueue
实现队列,则必须实现 offer
、 poll
和 peek
方法。
下面就正紧的分析 PriorityQueue
的源码了。
纵观源码,发现PriorityQueue是通过“极大优先级堆”实现的,即堆顶元素是优先级最大的元素。算是集成了大根堆和小根堆的功能。
根据堆的特性,存储结构肯定是数组了;既然支持不同优先级,肯定有比较器,也就是说支持自定义排序和顺序排序。
// 默认容量11 private static final int DEFAULT_INITIAL_CAPACITY = 11; // 堆的存储结构,存储元素 transient Object[] queue; // 不可序列化 // 当前存储的元素数量 int size; // 比较器,确定优先级高低 private final Comparator<? super E> comparator; // 避免OOM,数组可以分配的最大容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 复制代码
PriorityQueue的构造函数有很多,主要参数是容量和比较器:
public PriorityQueue() { this(DEFAULT_INITIAL_CAPACITY, null); // 默认自然序 } public PriorityQueue(int initialCapacity) { this(initialCapacity, null); // 自定义初始容量 } public PriorityQueue(Comparator<? super E> comparator) { this(DEFAULT_INITIAL_CAPACITY, comparator); // 自定义比较器 } public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { ... // 对应赋值 } public PriorityQueue(Collection<? extends E> c) { // 将c转成堆 if (c instanceof SortedSet<?>) { ... // SortedSet自带比较器 } else if (c instanceof PriorityQueue<?>) { ... // PriorityQueue自带比较器 } else { ... } } public PriorityQueue(PriorityQueue<? extends E> c) { ... // 从PriorityQueue获取比较器,将c转成堆 } public PriorityQueue(SortedSet<? extends E> c) { ...// 从SortedSet获取比较器,将c转成堆 } 复制代码
PriorityQueue是支持扩容的,先来看扩容方法:
// minCapacity表示需要的最小容量 private void grow(int minCapacity) { int oldCapacity = queue.length; // 获取当前容量 // Double size if small; else grow by 50% // 如果旧容量小于64,则增加旧容量+2的大小 // 如果旧容量大于等于64,则增加旧容量的一半大小 int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // overflow-conscious code if (newCapacity - MAX_ARRAY_SIZE > 0) // 这里处理大容量 newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); // 复制已存储的数据 } 复制代码
每次扩展的容量大小还是挺大的,要么变为原来的双倍,要么增长一半大小。
在看增加元素、删除元素和获取元素的方法之前,先了解以下几点:
public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); // 如果超出当前容量,则进行扩容 siftUp(i, e); // 新元素都是增加在数组尾部,然后进行上移操作,即构建堆 size = i + 1; // 当前大小加1 return true; } 复制代码
siftUp
方法最终会调用 siftUpUsingComparator
或者 siftUpComparable
方法,两个实现类似,这里直接看 siftUpUsingComparator
方法。
// 上移就是不断和父结点比较 private static <T> void siftUpUsingComparator( int k, T x, Object[] es, Comparator<? super T> cmp) { while (k > 0) { int parent = (k - 1) >>> 1; // 父结点下标 Object e = es[parent]; if (cmp.compare(x, (T) e) >= 0) // 优先级高则继续上移比较 break; es[k] = e; k = parent; } es[k] = x; } 复制代码
每次增加元素,都要保证堆序。
public E poll() { final Object[] es; final E result; // 返回堆顶元素 if ((result = (E) ((es = queue)[0])) != null) { modCount++; final int n; final E x = (E) es[(n = --size)]; // 把尾部元素换到第一个 es[n] = null; if (n > 0) { final Comparator<? super E> cmp; if ((cmp = comparator) == null) // 自然序时,下移调整 siftDownComparable(0, x, es, n); else // 自定义序下移调整 siftDownUsingComparator(0, x, es, n, cmp); } } return result; } 复制代码
poll方法会返回队首元素(堆顶),并将元素从堆中删除。删除过程,是将第一个元素与最后一个元素进行替换,然后再进行下移调整操作。这里直接看 siftDownUsingComparator
方法:
private static <T> void siftDownUsingComparator( int k, T x, Object[] es, int n, Comparator<? super T> cmp) { // assert n > 0; int half = n >>> 1; // 最后一个非叶子结点下标(因为size已经减1了) while (k < half) { int child = (k << 1) + 1; // 左孩子 Object c = es[child]; int right = child + 1; // 右孩子 if (right < n && cmp.compare((T) c, (T) es[right]) > 0) c = es[child = right]; // 从左右孩子中挑选优先级高的 if (cmp.compare(x, (T) c) <= 0) break; es[k] = c; // 将目标元素下移 k = child; } es[k] = x; } 复制代码
每次弹出队首元素,都要进行堆调整。
poll方法可以看出是remove方法的特例,即固定删除第一个元素。
public boolean remove(Object o) { int i = indexOf(o); // 找到待删除元素位置 if (i == -1) return false; else { removeAt(i); // 删除指定位置元素 return true; } } 复制代码
调用了 removeAt
方法:
E removeAt(int i) { // assert i >= 0 && i < size; final Object[] es = queue; modCount++; int s = --size; // size已经减1 if (s == i) // removed last element es[i] = null; // 已经删除到最后一个元素 else { E moved = (E) es[s]; // 尾元素 es[s] = null; siftDown(i, moved); // 指定元素换尾元素,然后调整 if (es[i] == moved) { siftUp(i, moved); // 如果指定位置换成了尾元素(没有发生下移)则进行上移操作 if (es[i] != moved) return moved; } } return null; // 正常删除时返回null } 复制代码