前景
继续昨天安装UML插件,构建了大概的Queue类图,今天好好研究下细节
类图
Queue
所有的关于Queue的类都实现了Queue这个接口,我们先看看最基本的Queue都包含哪些内容
方法 | 作用 |
---|---|
add(E) | 入队 |
element() | 返回队列头 |
offer(E) | 入队 |
peek() | 返回队列头 |
poll() | 返回队列头并且移除它 |
remove() | 返回队列头并且移除它 |
这些方法的作用有重复的部分,比如基本的实现功能就是三个,入队,出队,返回队头,但是每一种都有两种实现,下面进行一下对比
add 和offer
this method is generally
* preferable to {@link #add}, which can fail to insert an element only
* by throwing an exception.
引用源码上的一句话,offer比add好一点的地方在于,add当队列满的时候会抛出异常,而offer只是会返回false
element 和 peek
element和peek的区别在于队列为空的时候peek返回null,element返回异常
@throws NoSuchElementException if this queue is empty
poll 和 remove
poll和remove的区别差不多,poll返回null,remove返回异常
小总结:
- offer+element+poll是不会返回异常的
- add+peek+remove是返回异常的
AbstractQueue
关于它我们一笔带过,实现的方法如下
抛出异常也只是用调用相对应的方法,然后再抛出异常,比如
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
BolckQueue
阻塞的队列用于并发中,都在java.util.concurrent这个包里面,让我们先偷偷lou一眼
为啥是偷偷lou一眼呢?因为笔者进度还在集合,并非到并发,等到了并发我们再回过头来好好看看
PriorityQueue
优先级队列相信学过数据结构的小朋友都不陌生了把~如果你还不知道优先级队列,是时候去补一下功课啦~引用源码中的第一句话作为我们的开篇
An unbounded priority {@linkplain Queue queue} based on a priority heap.
一个基于优先堆的无限大的优先队列,很明显了,它是一个没有队列大小,底层是堆的队列
维护的属性
private static final int DEFAULT_INITIAL_CAPACITY = 11; //初始容量11
transient Object[] queue; // 数组
int size; //大小
private final Comparator<? super E> comparator; // 比较器
构造器
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
@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);
}
}
基础的三种构造器,分别对应了容量、比较器的选择与否,从一个集合中生成优先级队列
如何从集合中生成优先级队列
初始化,两个方法
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c);
heapify();
}
// 负责集合->数组转变
private void initElementsFromCollection(Collection<? extends E> c) {
Object[] a = c.toArray();
// If c.toArray incorrectly doesn't return Object[], copy it.
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, a.length, Object[].class);
int len = a.length;
if (len == 1 || this.comparator != null)
for (Object e : a)
if (e == null)
throw new NullPointerException();
this.queue = a;
this.size = a.length;
}
// 将转变过来的数组构建成堆
@SuppressWarnings("unchecked")
private void heapify() {
final Object[] es = queue;
int i = (size >>> 1) - 1;
if (comparator == null)
for (; i >= 0; i--)
siftDownComparable(i, (E) es[i]);
else
for (; i >= 0; i--)
siftDownUsingComparator(i, (E) es[i]);
}
这个构建堆的过程,如果不熟悉,是要去好好复习一下的,这里做简述,从叶子节点的上一层开始,一层一层构建小堆,同时构建的时候维护下堆顶的大小做一次向下筛选,如此循环直到顶层。
add
public boolean add(E e) {
return offer(e);
}
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;
return true;
}
add做的操作主要是,数组满扩容,和向上筛选的动作,向上筛选是数据结构的部分,不做探讨,看看扩容
扩容
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
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);
}
优先级队列的扩容比较有意思,当旧容量小于64的时候,新容量=旧容量×2+2,否则就是1.5×旧容量
peek
@SuppressWarnings("unchecked")
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
数组的尾部作为了队列的头
poll
@SuppressWarnings("unchecked")
public E poll() {
if (size == 0)
return null;
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;
}
poll移除队列头,并返回,将数组的最后一个元素取出作为堆顶,然后做向下筛选,维持堆序列
总结
由于PriorityQueue使用的是数组,数据结构是堆,所以描述的部分不详细,如果对堆的内容还不清楚,需要去好好看看的,我们总结一下几点
- PriorityQueue,底层是数组构建的堆,没有限制的容量大小
- 三种方法offer、pee、poll都不会抛出异常
- 扩容机制1.5倍,如果小于64 2倍+1
Deque
Deque俗称双端队列,之前知道有这样一个东西存在,但是没有真正接触到它,今天正好借机会来学习一下。
数据结构
ArrayDeque顾名思义,是基于数组的一个双端队列,基本的数组结构是这样的
head:是队列头的index,每次index往前移动一次,比如1 -> 0,head永远指向队列的头部,PS,是有元素的
tail: 队尾的index,永远指向null,区别head的指向头元素
维护的属性
transient Object[] elements;
transient int head;
transient int tail;
维护的属性很简单,只有3个,一个数组,一个头,一个尾
当做普通的队列使用
之前的时候,正如上文所言,没有Queue的实现,只有Deque和PriorityQueue,这两种,所以Deque里面实现了Queue的所有功能,回顾一下Queue中的几种方法
方法 | 作用 |
---|---|
add(E) | 入队 |
element() | 返回队列头 |
offer(E) | 入队 |
peek() | 返回队列头 |
poll() | 返回队列头并且移除它 |
remove() | 返回队列头并且移除它 |
Queue-入队
入队有add和offer这两种方法,但是在这里,它们最终都是同一个方法实现的,所以说它们没有区别,看看源代码
public boolean add(E e) {
addLast(e);
return true;
}
public boolean offer(E e) {
return offerLast(e);
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
可以看到它们的最底层都是addLast实现的
分析addLast:
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
final Object[] es = elements;
es[tail] = e;
if (head == (tail = inc(tail, es.length)))
grow(1);
}
刚才我们已经提到过,tail永远指向队列尾部元素的下一个位置null,所以直接es[tail] = e;
下面一句话是关键,做了两个操作
tail = inc(tail, es.length)
tail指针移动:
static final int inc(int i, int modulus) {
if (++i >= modulus) i = 0;
return i;
}
首先++i将tail的index+1,之后判断是否超出了数组的最大索引,超出了直接设置为0,等于就是形成了一个循环
队列满的调节及扩容:
if (head == (tail = inc(tail, es.length)))
grow(1);
if语句看head和tail是否重合,重合即扩容,双端队列的扩容比较厉害,笔者佩服
private void grow(int needed) {
// overflow-conscious code
final int oldCapacity = elements.length;
int newCapacity;
// Double capacity if small; else grow by 50%
int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);
if (jump < needed
|| (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
newCapacity = newCapacity(needed, jump);
final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
// Exceptionally, here tail == head needs to be disambiguated
if (tail < head || (tail == head && es[head] != null)) {
// wrap around; slide first leg forward to end of array
int newSpace = newCapacity - oldCapacity;
System.arraycopy(es, head,
es, head + newSpace,
oldCapacity - head);
for (int i = head, to = (head += newSpace); i < to; i++)
es[i] = null;
}
}
private int newCapacity(int needed, int jump) {
final int oldCapacity = elements.length, minCapacity;
if ((minCapacity = oldCapacity + needed) - MAX_ARRAY_SIZE > 0) {
if (minCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
return Integer.MAX_VALUE;
}
if (needed > jump)
return minCapacity;
return (oldCapacity + jump - MAX_ARRAY_SIZE < 0)
? oldCapacity + jump
: MAX_ARRAY_SIZE;
}
首先,当满的时候,1传入扩容方法grow,作为needed,总的来说,扩容和PriorityQueue一样,小于64 2倍+1,不然就是1.5倍,复制操作,是将原来的数组先如数拷贝进新数组,之后在新数组上,将数组进行拷贝,如下图
第一次复制,从0-5复制进新数组,第二次复制从0-5复制到6-11,并且重置head和将0-5全部清空
出队
public E poll() {
return pollFirst();
}
public E pollFirst() {
final Object[] es;
final int h;
E e = elementAt(es = elements, h = head);
if (e != null) {
es[h] = null;
head = inc(h, es.length);
}
return e;
}
这个操作简单,head指针后移,返回head指向的元素,之后再清空null,poll和remove方法一样的
返回队头
peek和elment都很简单,返回head指向的元素就结束了
当做栈使用
Deque接口也实现了栈相关的操作
方法 | 作用 |
---|---|
push | 入栈 |
pop | 出栈 |
peek | 栈顶 |
入栈push
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
final Object[] es = elements;
es[head = dec(head, es.length)] = e;
if (head == tail)
grow(1);
}
static final int dec(int i, int modulus) {
if (--i < 0) i = modulus - 1;
return i;
}
从源码我们可以分析出几点
1、head 作为栈顶来使用的
2、入栈首先将head前移一个指向新的元素,并且判断是否小于0,小于0 则在数组的尾部放入新的元素
3、由于tail指向最后一个元素的后一个位置,所以head和tail重合的时候,它先存入元素再判断head和tail的关系并不会导致新元素覆盖旧元素的问题。
出栈
public E pollFirst() {
final Object[] es;
final int h;
E e = elementAt(es = elements, h = head);
if (e != null) {
es[h] = null;
head = inc(h, es.length);
}
return e;
}
出栈就比较好理解了,移除head元素,并且head后移
双端队列
在理解了一端队列使用的情况下,我们很容易理解双端的含义,即head和tail的作用变成了差不多,既可以在左添加也可以在右边添加。如此而已,略过
总结
此次学习的重点是,我们可以看到队列的数组构建,里面的逻辑部分,对于熟悉的队列,java的优先级队列是基于堆的,堆呢又是基于数组的。最重要的还是复习巩固如何通过数组来实现队列。