java集合类深入分析之Queue篇
简介
Queue是一种很常见的数据结构类型,在java里面Queue是一个接口,它只是定义了一个基本的Queue应该有哪些功能规约。实际上有多个Queue的实现,有的是采用线性表实现,有的基于链表实现。还有的适用于多线程的环境。java中具有Queue功能的类主要有如下几个:AbstractQueue, ArrayBlockingQueue, ConcurrentLinkedQueue, LinkedBlockingQueue, DelayQueue, LinkedList, PriorityBlockingQueue, PriorityQueue和ArrayDqueue。在本文中,我们主要讨论常用的两种实现:LinkedList和ArrayDeque。
Queue
Queue本身是一种先入先出的模型(FIFO),和我们日常生活中的排队模型很类似。根据不同的实现,他们主要有数组和链表两种实现形式。如下图:


因为在队列里和我们日常的模型很近似,每次如果要出队的话,都是从队头移除。而如果每次要加入新的元素,则要在队尾加。所以我们要在队列里保存队头和队尾。
在jdk里几个常用队列实现之间的类关系图如下:

我们可以看到,Deque也是一个接口,它继承了Queue的接口规范。我们要讨论的LinkedList和ArrayDeque都是实现Deque接口,所以,可以说他们俩都是双向队列。具体的实现我们会在后面讨论。Queue作为一个接口,它声明的几个基本操作无非就是入队和出队的操作,具体定义如下:
public interface Queue<E> extends Collection<E> {
boolean add(E e); // 添加元素到队列中,相当于进入队尾排队。
boolean offer(E e); //添加元素到队列中,相当于进入队尾排队.
E remove(); //移除队头元素
E poll(); //移除队头元素
E element(); //获取但不移除队列头的元素
E peek(); //获取但不移除队列头的元素
}
有了这些接口定义的规约,我们就可以很容易的在后续的详细实现里察看具体细节。
Deque
按照我们一般的理解,Deque是一个双向队列,这将意味着它不过是对Queue接口的增强。如果仔细分析Deque接口代码的话,我们会发现它里面主要包含有4个部分的功能定义。1. 双向队列特定方法定义。 2. Queue方法定义。 3. Stack方法定义。 4. Collection方法定义。
第3,4部分的方法相当于告诉我们,具体实现Deque的类我们也可以把他们当成Stack和普通的Collection来使用。这也是接口定义规约带来的好处。这里我们就不再赘述。
我们重点来对Queue相关的定义方法做一下概括:
add相关的方法有如下几个:
boolean add(E e); boolean offer(E e); void addFirst(E e); void addLast(E e); boolean offerFirst(E e); boolean offerLast(E e);
这里定义了add, offer两个方法,从doc说明上来看,两者的基本上没什么区别。之所以定义了这两个方法是因为Deque继承了Collection, Queue两个接口,而这两个接口中都定义了增加元素的方法声明。他们本身的目的是一样的,只是在队列里头,添加元素肯定只是限于在队列的头或者尾添加。而offer作为一个更加适用于队列场景中的方法,也有存在的意义。他们的实现基本上一样,只是名字不同罢了。
remove相关的方法:
E removeFirst(); E removeLast(); E pollFirst(); E pollLast(); E remove(); E poll();
这里remove相关的方法poll和remove也很类似,他们存在的原因也和前面一样。
get元素相关的方法:
E getFirst(); E getLast(); E peekFirst(); E peekLast(); E element(); E peek();
peek和element方法和前面提到的差别有点不一样,element方法是在队列为空的时候抛异常,而element则是返回null。
ok,有了前面这些对方法操作的分门别类,我们后面分析起具体实现就更方便了。
ArrayDeque
有了我们前面几篇分析的基础,我们可以很容易猜到ArrayDeque的内部实现机制。它的内部使用一个数组来保存具体的元素,然后分别使用head, tail来指示队列的头和尾。他们的定义如下:
private transient E[] elements; private transient int head; private transient int tail; private static final int MIN_INITIAL_CAPACITY = 8;
ArrayDeque的默认长度为8,这么定义成2的指数值也是有一定好处的。在后面调整数组长度的时候我们会看到。关于tail需要注意的一点是tail所在的索引位置是null值,在它前面的元素才是队列中排在最后的元素。
调整元素长度
在调整元素长度部分,ArrayDeque采用了两个方法来分配。一个是allocateElements,还有一个是doubleCapacity。allocateElements方法用于构造函数中根据指定的参数设置初始数组的大小。而doubleCapacity则用于当数组元素不够用了扩展数组长度。
下面是allocateElements方法的实现:
private void allocateElements(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
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];
}
这部分代码里最让人困惑的地方就是对initialCapacity做的这一大堆移位和或运算。首先通过无符号右移1位,与原来的数字做或运算,然后在右移2、4、8、16位。这么做的目的是使得最后生成的数字尽可能每一位都是1。而且很显然,如果这个数字是每一位都为1,后面再对这个数字加1的话,则生成的数字肯定为2的若干次方。而且这个数字也肯定是大于我们的numElements值的最小2的指数值。这么说来有点绕。我们前面折腾了大半天,就为了求一个2的若干次方的数字,使得它要大于我们指定的数字,而且是最接近这个数字的数。这样子到底是为什么呢?因为我们后面要扩展数组长度的话,有了它这个基础我们就可以判断这个数字是不是到了2的多少多少次方,它增长下去最大的极限也不过是2的31次方。这样他每次的增长刚好可以把数组可以允许的长度给覆盖了,不会出现空间的浪费。比如说,我正好有一个数组,它的长度比Integer.MAX_VALUE的一半要大几个元素,如果我们这个时候设置的值不是让它为2的整数次方,那么直接对它空间翻倍就导致空间不够了,但是我们完全可以设置足够空间来容纳的。
我们现在再来看doubleCapacity方法:
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;
}
有了前面的讨论,它只要扩展空间容量的时候左移一位,这就相当于空间翻倍了。如果长度超出了允许的范围,就会发生溢出,返回的结果就会成为一个负数。这就是为什么有 if (newCapacity < 0)这一句来抛异常。
添加元素
我们先看看两个主要添加元素的方法add和offer:
public boolean add(E e) {
addLast(e);
return true;
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
public boolean offer(E e) {
return offerLast(e);
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
很显然,他们两个方法的底层实现实际上是一样的。这里要注意的一个地方就是我们由于不断的入队和出队,可能head和tail都会移动到超过数组的末尾。这个时候如果有空闲的空间,我们会把头或者尾跳到数组的头开始继续移动。所以添加元素并确定元素的下标是一个将元素下标值和数组长度进行求模运算的过程。addLast方法通过和当前数组长度减1求与运算来得到最新的下标值。它的效果相当于tail = (tail + 1) % elements.length;
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
addFirst和offerFirst是在head元素的之前插入元素,所以他们的位置为 (head - 1) & (elements.length - 1)。
取元素
获取元素主要包括如下几个方法:
public E element() {
return getFirst();
}
public E getFirst() {
E x = elements[head];
if (x == null)
throw new NoSuchElementException();
return x;
}
public E peek() {
return peekFirst();
}
public E peekFirst() {
return elements[head]; // elements[head] is null if deque empty
}
public E getLast() {
E x = elements[(tail - 1) & (elements.length - 1)];
if (x == null)
throw new NoSuchElementException();
return x;
}
public E peekLast() {
return elements[(tail - 1) & (elements.length - 1)];
}
这部分代码算是最简单的,无非就是取tail元素或者head元素的值。
关于队列的几种运算方法定义的特别杂乱,很容易让人搞混。如果从一个最简单的单向队列角度来看的话,我们可以把Queue中的enqueue方法对应到addLast方法,因为我们每次添加元素就是在队尾增加。deque方法则对应到removeFirst方法。虽然也可以用其他的方法来实现,不过具体的实现细节和他们基本上是一样的。
LinkedList
现在,我们在来看看LinkedList对应Queue的实现部分。在前面一篇文章中,已经讨论过LinkedList里面Node的结构。它本身包含元素值,prev、next两个引用。对链表的增加和删除元素的操作不像数组,不存在要考虑下标的问题,也不需要扩展数组空间,因此就简单了很多。先看查找元素部分:
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
这里唯一值得注意的一点就是last引用是指向队列最末尾和元素,和前面ArrayDeque的情况不一样。
添加元素的方法如下:
linkLast的方法在前一篇文章里已经分析过,就不再重复。
删除元素的方法主要有remove():
这部分的代码看似比较长,实际上是遍历整个链表,如果找到要删除的元素,则移除该元素。这部分的难点在unlink方法里面。我们分别用要删除元素的前面和后面的引用来判断各种当prev和next为null时的各种情况。虽然不是很复杂,但是很繁琐。
总结
从我们实际中的考量来看,Queue和Deque他们本身不仅定义了作为一个队列需要的基本功能。同时因为队列也是属于整个集合类这一个大族里面的,所以他们也必须要具备集合类的一些常用功能,比如元素查找,删除,迭代器等。我们读一些集合类的代码时,尤其是一些接口的定义,会发现一个比较有意思的事情。就是通常一些子接口把父接口的方法又重新定义了一遍。这样似乎违背了面向对象里继承的原则。后来经过一些讨论,发现主要原因是一些jdk版本的更新,有的新类是后面新增加的。这些新的接口有的是为了保持兼容,有的是为了保证后续生成文档里方便用户知道它也有同样的功能而不需要再去查它的父类,就直接把父类的东西给搬过来了。比较有意思,读代码还读出点历史感了。
参考资料
http://docs.oracle.com/javase/7/docs/api/java/util/Queue.html
http://docs.oracle.com/javase/7/docs/api/java/util/Deque.html
http://blog.donews.com/maverick/archive/2005/08/31/534269.aspx
java集合类深入分析之Queue篇的更多相关文章
- java集合类深入分析之Queue篇(Q,DQ)
简介 Queue是一种很常见的数据结构类型,在java里面Queue是一个接口,它只是定义了一个基本的Queue应该有哪些功能规约.实际上有多个Queue的实现,有的是采用线性表实现,有的基于链表实现 ...
- Java集合类汇总记录--JDK篇
接口类图 Java Collection由两套并行的接口组成,一套是Collection接口,一套是Map接口.例如以下图 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkb ...
- java集合类深入分析之PriorityQueue(二)
PriorityQueue介绍 在平时的编程工作中似乎很少碰到PriorityQueue(优先队列) ,故很多人一开始看到优先队列的时候还会有点迷惑.优先队列本质上就是一个最小堆.前面一篇文章介绍了堆 ...
- Java集合类汇总记录--guava篇
BiMap HashBiMap<K,V> 实现了两份哈希表数据结构(本类独立实现).分别负责两个方向的映射. EnumBiMap<K,V> 两个EnumMap对象分别负责两个方 ...
- Java集合类常见面试知识点总结
微信公众号[Java技术江湖]一位阿里Java工程师的技术小站 Java集合类学习总结 这篇总结是基于之前博客内容的一个整理和回顾. 这里先简单地总结一下,更多详细内容请参考我的专栏:深入浅出Java ...
- Java集合类: Set、List、Map、Queue使用场景梳理
本文主要关注Java编程中涉及到的各种集合类,以及它们的使用场景 相关学习资料 http://files.cnblogs.com/LittleHann/java%E9%9B%86%E5%90%88%E ...
- Java集合类: Set、List、Map、Queue使用
目录 1. Java集合类基本概念 2. Java集合类架构层次关系 3. Java集合类的应用场景代码 1. Java集合类基本概念 在编程中,常常需要集中存放多个数据.从传统意义上讲,数组是我们的 ...
- 基础知识《六》---Java集合类: Set、List、Map、Queue使用场景梳理
本文转载自LittleHann 相关学习资料 http://files.cnblogs.com/LittleHann/java%E9%9B%86%E5%90%88%E6%8E%92%E5%BA%8F% ...
- java集合类(六)About Queue
接上篇“java集合类(五)About Map” 终于来到了java集合类的尾声,太兴奋了,不是因为可以休息一阵了,而是因为又到了开启新知识的时刻,大家一起加油打气!!Come on...Fighti ...
随机推荐
- 深入理解DOM节点关系
× 目录 [1]父级属性 [2]子级属性 [3]同级属性[4]包含方法[5]关系方法 前面的话 DOM可以将任何HTML描绘成一个由多层节点构成的结构.节点分为12种不同类型,每种类型分别表示文档中不 ...
- JavaScript 数据属性和访问器属性
在JavaScript中对象被定义为"无序属性的集合,其属性可以包含基本值.对象或函数."通俗点讲,我们可以把对象理解为一组一组的名值对,其中值可以是数据或函数. 创建自定义对象通 ...
- Atitit ati licenseService 设计原理
Atitit ati licenseService 设计原理 C:\0workspace\AtiPlatf\src_atibrow\com\attilax\license\LicenseX.ja ...
- Retrofit结合RxJava使用指南
Retrofit结合RxJava使用指南 Retrofit是一个当前很流行的网络请求库, 官网的介绍是: "Type-safe HTTP client for Android and Jav ...
- Java之递归求和的两张方法
方法一: package com.smbea.demo; public class Student { private int sum = 0; /** * 递归求和 * @param num */ ...
- ASP.NET Button、ImageButton、LinkButton、HyperLink区别
这4个控件都属于WEB服务器控件,有很多相同的属性和事件.其区别如下所示. 在*.aspx页面中插入Button控件如以下代码所示.<asp:Button runat="server& ...
- python 进度条的编写
背景: 在执行一些Python脚本时,经常出现执行脚本的过程当中,不知道脚本执行了百分之多少,这个问题一直都让我很苦恼.所以特意总结一下,进度条的编写. #!/usr/bin/env python2. ...
- Spring ApplicationContext 简解
ApplicationContext是对BeanFactory的扩展,实现BeanFactory的所有功能,并添加了事件传播,国际化,资源文件处理等. configure locations:(C ...
- 基于jquery实现图片拖动和曲线拖放
功能:图片的拖动.曲线的拖放和绘制 一. 准备工作 1. 点击此下载相关的文档 二. 在浏览器中运行 dragDrop.html 文件,即可看到效果 三. 效果图
- Linux操作系统主机名(hostname)简介
http://www.jb51.net/LINUXjishu/10938.html 摘要:本文是关于Linux操作系统主机名(hostname)的文档,对主要配置文件/etc/hosts进行简要的说明 ...