简介

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作为一个接口,它声明的几个基本操作无非就是入队和出队的操作,具体定义如下:

  1. public interface Queue<E> extends Collection<E> {
  2. boolean add(E e); // 添加元素到队列中,相当于进入队尾排队。
  3. boolean offer(E e);  //添加元素到队列中,相当于进入队尾排队.
  4. E remove(); //移除队头元素
  5. E poll();  //移除队头元素
  6. E element(); //获取但不移除队列头的元素
  7. E peek();  //获取但不移除队列头的元素
  8. }

有了这些接口定义的规约,我们就可以很容易的在后续的详细实现里察看具体细节。

Deque

按照我们一般的理解,Deque是一个双向队列,这将意味着它不过是对Queue接口的增强。如果仔细分析Deque接口代码的话,我们会发现它里面主要包含有4个部分的功能定义。1. 双向队列特定方法定义。 2. Queue方法定义。 3. Stack方法定义。 4. Collection方法定义。

第3,4部分的方法相当于告诉我们,具体实现Deque的类我们也可以把他们当成Stack和普通的Collection来使用。这也是接口定义规约带来的好处。这里我们就不再赘述。

我们重点来对Queue相关的定义方法做一下概括:

add相关的方法有如下几个:

  1. boolean add(E e);
  2. boolean offer(E e);
  3. void addFirst(E e);
  4. void addLast(E e);
  5. boolean offerFirst(E e);
  6. boolean offerLast(E e);

这里定义了add, offer两个方法,从doc说明上来看,两者的基本上没什么区别。之所以定义了这两个方法是因为Deque继承了Collection, Queue两个接口,而这两个接口中都定义了增加元素的方法声明。他们本身的目的是一样的,只是在队列里头,添加元素肯定只是限于在队列的头或者尾添加。而offer作为一个更加适用于队列场景中的方法,也有存在的意义。他们的实现基本上一样,只是名字不同罢了。

remove相关的方法:

  1. E removeFirst();
  2. E removeLast();
  3. E pollFirst();
  4. E pollLast();
  5. E remove();
  6. E poll();

这里remove相关的方法poll和remove也很类似,他们存在的原因也和前面一样。

get元素相关的方法:

  1. E getFirst();
  2. E getLast();
  3. E peekFirst();
  4. E peekLast();
  5. E element();
  6. E peek();

peek和element方法和前面提到的差别有点不一样,element方法是在队列为空的时候抛异常,而element则是返回null。

ok,有了前面这些对方法操作的分门别类,我们后面分析起具体实现就更方便了。

ArrayDeque

有了我们前面几篇分析的基础,我们可以很容易猜到ArrayDeque的内部实现机制。它的内部使用一个数组来保存具体的元素,然后分别使用head, tail来指示队列的头和尾。他们的定义如下:

  1. private transient E[] elements;
  2. private transient int head;
  3. private transient int tail;
  4. private static final int MIN_INITIAL_CAPACITY = 8;

ArrayDeque的默认长度为8,这么定义成2的指数值也是有一定好处的。在后面调整数组长度的时候我们会看到。关于tail需要注意的一点是tail所在的索引位置是null值,在它前面的元素才是队列中排在最后的元素。

调整元素长度

在调整元素长度部分,ArrayDeque采用了两个方法来分配。一个是allocateElements,还有一个是doubleCapacity。allocateElements方法用于构造函数中根据指定的参数设置初始数组的大小。而doubleCapacity则用于当数组元素不够用了扩展数组长度。

下面是allocateElements方法的实现:

  1. private void allocateElements(int numElements) {
  2. int initialCapacity = MIN_INITIAL_CAPACITY;
  3. // Find the best power of two to hold elements.
  4. // Tests "<=" because arrays aren't kept full.
  5. if (numElements >= initialCapacity) {
  6. initialCapacity = numElements;
  7. initialCapacity |= (initialCapacity >>>  1);
  8. initialCapacity |= (initialCapacity >>>  2);
  9. initialCapacity |= (initialCapacity >>>  4);
  10. initialCapacity |= (initialCapacity >>>  8);
  11. initialCapacity |= (initialCapacity >>> 16);
  12. initialCapacity++;
  13. if (initialCapacity < 0)   // Too many elements, must back off
  14. initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
  15. }
  16. elements = (E[]) new Object[initialCapacity];
  17. }

这部分代码里最让人困惑的地方就是对initialCapacity做的这一大堆移位和或运算。首先通过无符号右移1位,与原来的数字做或运算,然后在右移2、4、8、16位。这么做的目的是使得最后生成的数字尽可能每一位都是1。而且很显然,如果这个数字是每一位都为1,后面再对这个数字加1的话,则生成的数字肯定为2的若干次方。而且这个数字也肯定是大于我们的numElements值的最小2的指数值。这么说来有点绕。我们前面折腾了大半天,就为了求一个2的若干次方的数字,使得它要大于我们指定的数字,而且是最接近这个数字的数。这样子到底是为什么呢?因为我们后面要扩展数组长度的话,有了它这个基础我们就可以判断这个数字是不是到了2的多少多少次方,它增长下去最大的极限也不过是2的31次方。这样他每次的增长刚好可以把数组可以允许的长度给覆盖了,不会出现空间的浪费。比如说,我正好有一个数组,它的长度比Integer.MAX_VALUE的一半要大几个元素,如果我们这个时候设置的值不是让它为2的整数次方,那么直接对它空间翻倍就导致空间不够了,但是我们完全可以设置足够空间来容纳的。

我们现在再来看doubleCapacity方法:

  1. private void doubleCapacity() {
  2. assert head == tail;
  3. int p = head;
  4. int n = elements.length;
  5. int r = n - p; // number of elements to the right of p
  6. int newCapacity = n << 1;
  7. if (newCapacity < 0)
  8. throw new IllegalStateException("Sorry, deque too big");
  9. Object[] a = new Object[newCapacity];
  10. System.arraycopy(elements, p, a, 0, r);
  11. System.arraycopy(elements, 0, a, r, p);
  12. elements = (E[])a;
  13. head = 0;
  14. tail = n;
  15. }

有了前面的讨论,它只要扩展空间容量的时候左移一位,这就相当于空间翻倍了。如果长度超出了允许的范围,就会发生溢出,返回的结果就会成为一个负数。这就是为什么有 if (newCapacity < 0)这一句来抛异常。

添加元素

我们先看看两个主要添加元素的方法add和offer:

  1. public boolean add(E e) {
  2. addLast(e);
  3. return true;
  4. }
  5. public void addLast(E e) {
  6. if (e == null)
  7. throw new NullPointerException();
  8. elements[tail] = e;
  9. if ( (tail = (tail + 1) & (elements.length - 1)) == head)
  10. doubleCapacity();
  11. }
  12. public boolean offer(E e) {
  13. return offerLast(e);
  14. }
  15. public boolean offerLast(E e) {
  16. addLast(e);
  17. return true;
  18. }

很显然,他们两个方法的底层实现实际上是一样的。这里要注意的一个地方就是我们由于不断的入队和出队,可能head和tail都会移动到超过数组的末尾。这个时候如果有空闲的空间,我们会把头或者尾跳到数组的头开始继续移动。所以添加元素并确定元素的下标是一个将元素下标值和数组长度进行求模运算的过程。addLast方法通过和当前数组长度减1求与运算来得到最新的下标值。它的效果相当于tail = (tail + 1) % elements.length;

  1. public void addFirst(E e) {
  2. if (e == null)
  3. throw new NullPointerException();
  4. elements[head = (head - 1) & (elements.length - 1)] = e;
  5. if (head == tail)
  6. doubleCapacity();
  7. }
  8. public boolean offerFirst(E e) {
  9. addFirst(e);
  10. return true;
  11. }

addFirst和offerFirst是在head元素的之前插入元素,所以他们的位置为 (head - 1) & (elements.length - 1)。

取元素

获取元素主要包括如下几个方法:

  1. public E element() {
  2. return getFirst();
  3. }
  4. public E getFirst() {
  5. E x = elements[head];
  6. if (x == null)
  7. throw new NoSuchElementException();
  8. return x;
  9. }
  10. public E peek() {
  11. return peekFirst();
  12. }
  13. public E peekFirst() {
  14. return elements[head]; // elements[head] is null if deque empty
  15. }
  16. public E getLast() {
  17. E x = elements[(tail - 1) & (elements.length - 1)];
  18. if (x == null)
  19. throw new NoSuchElementException();
  20. return x;
  21. }
  22. public E peekLast() {
  23. return elements[(tail - 1) & (elements.length - 1)];
  24. }

这部分代码算是最简单的,无非就是取tail元素或者head元素的值。

关于队列的几种运算方法定义的特别杂乱,很容易让人搞混。如果从一个最简单的单向队列角度来看的话,我们可以把Queue中的enqueue方法对应到addLast方法,因为我们每次添加元素就是在队尾增加。deque方法则对应到removeFirst方法。虽然也可以用其他的方法来实现,不过具体的实现细节和他们基本上是一样的。

LinkedList

现在,我们在来看看LinkedList对应Queue的实现部分。在前面一篇文章中,已经讨论过LinkedList里面Node的结构。它本身包含元素值,prev、next两个引用。对链表的增加和删除元素的操作不像数组,不存在要考虑下标的问题,也不需要扩展数组空间,因此就简单了很多。先看查找元素部分:

  1. public E getFirst() {
  2. final Node<E> f = first;
  3. if (f == null)
  4. throw new NoSuchElementException();
  5. return f.item;
  6. }
  7. public E getLast() {
  8. final Node<E> l = last;
  9. if (l == null)
  10. throw new NoSuchElementException();
  11. return l.item;
  12. }

这里唯一值得注意的一点就是last引用是指向队列最末尾和元素,和前面ArrayDeque的情况不一样。

添加元素的方法如下:

  1. public boolean add(E e) {
  2. linkLast(e);
  3. return true;
  4. }
  5. public boolean offer(E e) {
  6. return add(e);
  7. }
  8. void linkLast(E e) {
  9. final Node<E> l = last;
  10. final Node<E> newNode = new Node<>(l, e, null);
  11. last = newNode;
  12. if (l == null)
  13. first = newNode;
  14. else
  15. l.next = newNode;
  16. size++;
  17. modCount++;
  18. }

linkLast的方法在前一篇文章里已经分析过,就不再重复。

删除元素的方法主要有remove():

  1. public boolean remove(Object o) {
  2. if (o == null) {
  3. for (Node<E> x = first; x != null; x = x.next) {
  4. if (x.item == null) {
  5. unlink(x);
  6. return true;
  7. }
  8. }
  9. } else {
  10. for (Node<E> x = first; x != null; x = x.next) {
  11. if (o.equals(x.item)) {
  12. unlink(x);
  13. return true;
  14. }
  15. }
  16. }
  17. return false;
  18. }
  19. E unlink(Node<E> x) {
  20. // assert x != null;
  21. final E element = x.item;
  22. final Node<E> next = x.next;
  23. final Node<E> prev = x.prev;
  24. if (prev == null) {
  25. first = next;
  26. } else {
  27. prev.next = next;
  28. x.prev = null;
  29. }
  30. if (next == null) {
  31. last = prev;
  32. } else {
  33. next.prev = prev;
  34. x.next = null;
  35. }
  36. x.item = null;
  37. size--;
  38. modCount++;
  39. return element;
  40. }

这部分的代码看似比较长,实际上是遍历整个链表,如果找到要删除的元素,则移除该元素。这部分的难点在unlink方法里面。我们分别用要删除元素的前面和后面的引用来判断各种当prev和next为null时的各种情况。虽然不是很复杂,但是很繁琐。

总结

从我们实际中的考量来看,Queue和Deque他们本身不仅定义了作为一个队列需要的基本功能。同时因为队列也是属于整个集合类这一个大族里面的,所以他们也必须要具备集合类的一些常用功能,比如元素查找,删除,迭代器等。我们读一些集合类的代码时,尤其是一些接口的定义,会发现一个比较有意思的事情。就是通常一些子接口把父接口的方法又重新定义了一遍。这样似乎违背了面向对象里继承的原则。后来经过一些讨论,发现主要原因是一些jdk版本的更新,有的新类是后面新增加的。这些新的接口有的是为了保持兼容,有的是为了保证后续生成文档里方便用户知道它也有同样的功能而不需要再去查它的父类,就直接把父类的东西给搬过来了。比较有意思,读代码还读出点历史感了。

java集合类深入分析之Queue篇(Q,DQ)的更多相关文章

  1. java集合类深入分析之Queue篇

    简介 Queue是一种很常见的数据结构类型,在java里面Queue是一个接口,它只是定义了一个基本的Queue应该有哪些功能规约.实际上有多个Queue的实现,有的是采用线性表实现,有的基于链表实现 ...

  2. Java集合类汇总记录--JDK篇

    接口类图 Java Collection由两套并行的接口组成,一套是Collection接口,一套是Map接口.例如以下图 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkb ...

  3. java集合类深入分析之PriorityQueue(二)

    PriorityQueue介绍 在平时的编程工作中似乎很少碰到PriorityQueue(优先队列) ,故很多人一开始看到优先队列的时候还会有点迷惑.优先队列本质上就是一个最小堆.前面一篇文章介绍了堆 ...

  4. Java集合类汇总记录--guava篇

    BiMap HashBiMap<K,V> 实现了两份哈希表数据结构(本类独立实现).分别负责两个方向的映射. EnumBiMap<K,V> 两个EnumMap对象分别负责两个方 ...

  5. Java集合类常见面试知识点总结

    微信公众号[Java技术江湖]一位阿里Java工程师的技术小站 Java集合类学习总结 这篇总结是基于之前博客内容的一个整理和回顾. 这里先简单地总结一下,更多详细内容请参考我的专栏:深入浅出Java ...

  6. Java集合类: Set、List、Map、Queue使用场景梳理

    本文主要关注Java编程中涉及到的各种集合类,以及它们的使用场景 相关学习资料 http://files.cnblogs.com/LittleHann/java%E9%9B%86%E5%90%88%E ...

  7. Java集合类: Set、List、Map、Queue使用

    目录 1. Java集合类基本概念 2. Java集合类架构层次关系 3. Java集合类的应用场景代码 1. Java集合类基本概念 在编程中,常常需要集中存放多个数据.从传统意义上讲,数组是我们的 ...

  8. 基础知识《六》---Java集合类: Set、List、Map、Queue使用场景梳理

    本文转载自LittleHann 相关学习资料 http://files.cnblogs.com/LittleHann/java%E9%9B%86%E5%90%88%E6%8E%92%E5%BA%8F% ...

  9. java集合类(六)About Queue

    接上篇“java集合类(五)About Map” 终于来到了java集合类的尾声,太兴奋了,不是因为可以休息一阵了,而是因为又到了开启新知识的时刻,大家一起加油打气!!Come on...Fighti ...

随机推荐

  1. [工作问题总结]MyEclipse 注册

    ------------------------------ASP.Net+Android+IO开发 .Net培训 期待与您交流!------------------------------ 1.本人 ...

  2. Android应用程序组件Content Provider的启动过程源代码分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6963418 通过前面的学习,我们知道在Andr ...

  3. 基于express框架的应用程序骨架生成器介绍

    作者:zhanhailiang 日期:2014-11-09 本文将介绍怎样使用express-generator工具高速生成基于express框架的应用程序骨架: 1. 安装express-gener ...

  4. 面试前的准备---C#知识点回顾----01

    过完年来,准备找份新工作,虽然手里的工作不错,但树挪死,人挪活.咱不能一直在一个坑里生活一辈子,外面的世界毕竟是很美好的. 为了能正常的找到自己中意的工作,最近是将所有的基础知识拿出来复习了一次.仅作 ...

  5. 让资源可以下载a

    第一种方式------不存在任何兼容性 <a href='x.zip'>下载</a> 将要链接的资源进行打包即可 第二种方式----存在兼容性,目前只有Chrome 和Fire ...

  6. js 倒计时 已过去时间

    页面中的代码: <strong id="timer" datatime="2012-12-09 10:20:30"></strong> ...

  7. 目标管理剖析与实践– 献给追梦的人 (转)

      好久没写日志了. 最近总算在忙碌的日子中小小的松了一口气, 过来补起这几个月的空缺. 上次写的Cover Letter & Resume 重点诠释 - 深度剖析没想到居然超过了一万的阅读量 ...

  8. APP设计规范大全

    大图可保存到本地

  9. 在IE中调试Javascript

    不管我们写代码的时候如何小心,都不可能完全避免程序中出现bug,这个时侯就需要我们在调试的时候找出错误,修改代码. Javascript是一门灵活的语言,灵活的语法和它解释执行的特性,使得Javasc ...

  10. 前端利器,如何使用fiddle拦截在线css进行先下调试

    fiddle的功能相当的强悍,用户也非常广,不过今天我就教大家用fiddle进行前端调试. 首先下载软件fiddle,点击对应的版本下载安装. 安装成功后打开看到右侧的导航栏: 点击AutoRespo ...