Java 集合框架 Queue 和 Stack 体系

目录
  • Stack
  • Queue
    • Deque
    • 其他特性
  • BlockingQueue
    • 特点
  • PriorityQueue 优先级队列
    • 特点
    • 扩容机制
  • ArrayDeque
    • 继承关系
    • 底层实现
    • 扩容机制
  • 总结

Stack

栈结构类型,表示对象的后进先出堆栈。Stack 继承自 Vector ,并拓展了五个允许将容器视为栈结构的操作。

包括常见的 pop 和 push 操作、以及查看栈顶元素的方法、检查栈是否为空的方法以及从栈顶向下进行搜索某个元素,并获取该元素在栈内深度的方法。

Deque 接口及其实现提供了一组更完整的 LIFO 堆栈操作能力,应该优先考虑使用 Deque 及其实现。例如:

Deque<Integer> stack = new ArrayDeque<Integer>();

Queue

Queue 接口定义了队列的能力,它继承自 Collection ,除了基本的集合操作外,队列提供了额外的插入、获取、检查等操作。

public interface Queue<E> extends Collection<E> {
    // 在不违反容量限制的情况下立即将指定元素插入此队列,成功时返回 true,如果当前没有可用空间则抛出 IllegalStateException。
    boolean add(E e);
    // 在不违反容量限制的情况下立即将指定元素插入此队列。 在使用容量受限的队列中,一般最好使用这种方法添加,仅通过抛出异常可能无法插入元素。
    boolean offer(E e);
    // 检索并删除此队列的头部。 此方法与 poll 的不同之处仅在于如果此队列为空,它将引发异常。
    E remove();
    // 检索并移除此队列的头部,如果此队列为空,则返回 null。
    E poll();
    // 检索但不删除此队列的头部。 此方法与 peek 的不同之处仅在于如果此队列为空,它将引发异常。
    E element();
    // 检索但不删除此队列的头部,如果此队列为空,则返回 null。
    E peek();
}

可以看出,每一种操作都存在两种定义,一种是在操作失败的情况下强制抛出异常的,另一种则是返回 null 不强制抛出异常的。

  Throws exception Returns special value
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()

队列通常是先进先出的,所以对元素的排序方式也是以先进先出的顺序的,但是有特殊情况,例如,优先级队列是根据比较元素的优先级进行排序的;另一种例子是后进先出队列,即栈结构。

无论使用哪种排序,队列的头部元素都是通过调用 remove 或 poll 删除的。在 FIFO 队列中,所有的新元素都是插入到队列的尾部的,其他类型的队列可能使用不同的插入规则,每个 Queue 的实现都必须明确指定其排序方式。

队列的另一个特性是不允许插入 null ,尽管在某些实现中,例如 LinkedList,允许存储 null。但在这种实现中,也不应该将 null 插入到队列中,因为 null 被 poll 方法作为特殊返回值,以表明队列不包含任何元素。

队列的实现通常不定义 hashCode 和 equals ,因为对于具有相同的元素,但具有不同的排序方式的队列,基于元素的相等并不是很好的定义方式。

Deque

Deque 继承自 Queue ,是一种支持两端元素插入和移除的顺序结构集合,简称 “双端队列” 。大多数双端队列对容量没有固定限制。

Deque 接口主要拓展了对于双端队列两端元素操作的方法,也是两种形式,一种抛出异常,一种返回特殊值 null 的形式。

Deque 可以当作 FIFO 队列使用,所以它的一些拓展的方法等效于 Queue 的一些方法:

Deque 作为 LIFO 队列使用时(栈结构),它也会有一些等价于 Stack 的方法:

其他特性

与 List 接口不同,此接口不支持对元素的索引访问。

另外虽然没有严格要求 Deque 的实现禁止插入 null 元素,但在使用中的确应该尽量避免插入 null,因为 null 元素被各种操作方法作为特殊的返回值,这里和 Queue 一样。

另一个与 Queue 相同的是,它也没有定义需要实现 hash 和 equals 方法,理由和 Queue 是一样的。

BlockingQueue

BlockingQueue 是 Queue 接口的另一个重要子接口,它用来代表一个可阻塞的队列。支持在检索元素是,等待队列变为非空再返回数据。

BlockingQueue 有四种类型,用来解决当前不能立刻处理,需要再未来某个时间点下满足指定条件才执行的操作。

  • 抛出异常
  • 返回特殊值
  • 无限期阻塞当前线程,直到操作成功
  • 在有限时间内阻塞当前线程,超时即放弃执行

以下这张表是同一个行为在不同类型的方法表:

特点

  • 不支持空元素:BlockingQueue 不接受空元素。在尝试添加、放置或提供 null 时,实现会抛出 NullPointerException。 null 用作标记值以指示轮询操作失败。
  • 容量受限:可能是容量受限的,在给定时间内,可能只有一个剩余容量,超出该容量就不能在不阻塞的情况下放置其他元素。
  • 主要用于实现生产者-消费者队列,但还支持 Collection 接口。
  • 线程安全需要它的实现类自身去实现。
  • 批量操作不能保证原子性。
  • 内存一致性:和其他并发集合一样,在一个线程中实现将一个对象存入 BlockingQueue 的操作,会发生在下一个其他线程对 BlockingQueue 中的该元素读取或移除之前。

PriorityQueue 优先级队列

PriorityQueue 是一个基于优先级堆的无界限优先级队列。它是 Queue 的直接实现类,优先级队列的元素排序有两种情况:

  • 根据元素的自然存储顺序
  • 队列构造时提供的 Comparator 进行比较后排序

排序方式取决于具体的构造函数的参数 Comparator 。

    public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }

特点

优先级队列不允许 null 元素,依赖于自然排序的优先级队列也不允许插入不可比较的对象,否则会导致 ClassCastException 。

底层实现是通过数组来保存数据的:

transient Object[] queue;

不是线程安全的。

扩容机制

优先级队列的容量是没有限制的,但是内部存储元素的结构实际上是一个数组,它的扩容机制是有规律的。

初始化默认容量 11 ,后续扩容总是扩容到与队列元素数量相同大小,这一点和 ArrayList 基本一致。

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException(); // 不允许 null 元素
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1); // 真正的扩容方法
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }
    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 ,每次扩容 100% 并加 2 。超过 64,每次扩容旧容量的 50 %。
  • 如果新容量超出最大数组容量。则通过 hugeCapacity() 计算容量:
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
  • 如果需要的容量超过数组最大容量,则限制队列容量为 Int 的最大值。
  • 如果需要的容量没超过数组最大容量,则限制队列容量为数组最大容量 MAX_ARRAY_SIZE 。

ArrayDeque

数组双端队列,是一种可调整大小的数组实现,没有容量限制。它会根据需要自动扩容。

继承关系

public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable
  • AbstractCollection:提供了一些集合接口能力的基本封装。
  • 双端队列
  • 可通过 Object.clone() 方法快速复制
  • 支持序列化

底层实现

底层数据结构是一个数组:

transient Object[] elements;
transient int head;
transient int tail;

通过 head 和 tail 作为索引来支持双端队列的特性。

扩容机制

    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException(); // 不允许 null
        elements[head = (head - 1) & (elements.length - 1)] = e; // 防止下标越界处理
        if (head == tail) // 检查空间是否够用, == 说明空间不够了
            doubleCapacity(); // 扩容
    }

这里先插入,后扩容。tail 总是指向下一个可插入的空位,这个意思就是 数组中至少留有一个空位置,所以元素可以先插入。

head = (head - 1) & (elements.length - 1) 这行代码比较难以理解。语义是:取余操作,同时解决了head 为负值的情况。 elements.length 必定是 2 的指数倍。 elements.length - 1 的二进制低位必为 1 , 与head - 1 进行与操作后,起到了取模的作用(取余数)。如果 head - 1 为负数(只可能是 -1),则相当于对其取相对于 elements.length 的补码。

真正的扩容机制在 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 = a;
        head = 0;
        tail = n;
    }

这个函数的作用相当于扩容 100% 后,将原数组,分段复制进去:

首先复制 head 右侧的元素,然后再把左边的复制过来。即虽然是往队列头部插值,但实际还是在尾部插完值后,分段移动进行排序,最后组成了新数组。

addLast 则是直接把新元素存入tail 位置上,然后在重新计算 tail ,检查是否需要扩容。

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;   //赋值
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)   //下标越界处理
        doubleCapacity();   //扩容
}

特点:

  • 不是线程安全的,在没有外部处理同步的情况下,不支持多线程并发访问。
  • 禁止保存 null 元素。
  • 作为栈使用时,可能比 Stack 快;作为队列使用时, 比 LinkedList 快。
  • 时间复杂度时常数级别的。

总结

常见的队列除了 ArrayQueuePriorityQuueue 和 BlockingQueue,还有一个可以当作队列的 LinkedList,关于它在上一节的 List 体系中有详细的讲解。

  • ArrayQueue,更适用于当作双端队列或栈结构。
  • Stack 已不推荐使用,建议使用 LinkedList 或 ArrayQueue 替代。
  • PriorityQueue 用来处理优先级,多了一个可自定义优先级条件的能力。
  • BlockingQueue 常用于实现 生产者/消费者队列,但线程安全需要外部自己去实现。
  • 上面的几种队列,除了 LinkedList,底层的数据结构都是数组。
  • Queue 的有些实现没有强制要求不允许存 null ,但最好不要存 null 。

到此这篇关于Java 集合框架 Queue 和 Stack 体系的文章就介绍到这了,更多相关Java 集合框架 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java Stack与Queue详解

    一.Stack 示例: package StackPack; import java.util.Stack; public class StackDemo { public static void main(String[] args) { Stack<Integer> stack=new Stack<>(); for(int i=0;i<10;i++) { stack.push(i*2); } //[0, 2, 3, 4, 6, 8, 12, 14, 16, 18] Sys

  • Java集合框架之Stack Queue Deque使用详解刨析

    目录 1. Stack 1.1 介绍 1.2 常见方法 2. Queue 2.1 介绍 2.2 常见方法 3. Deque 3.1 介绍 3.2 常见方法 1. Stack 1.1 介绍 Stack 栈是 Vector 的一个子类,它实现了一个标准的后进先出的栈.它的底层是一个数组. 堆栈只定义了默认构造函数,用来创建一个空栈.堆栈除了包括由 Vector 定义的所有方法,也定义了自己的一些方法. 1.2 常见方法 方法 描述 E push(E item) 压栈 E pop() 出栈 E pee

  • java集合框架 arrayblockingqueue应用分析

    Queue ------------ 1.ArrayDeque, (数组双端队列) 2.PriorityQueue, (优先级队列) 3.ConcurrentLinkedQueue, (基于链表的并发队列) 4.DelayQueue, (延期阻塞队列)(阻塞队列实现了BlockingQueue接口) 5.ArrayBlockingQueue, (基于数组的并发阻塞队列) 6.LinkedBlockingQueue, (基于链表的FIFO阻塞队列) 7.LinkedBlockingDeque, (

  • Java 集合框架 Queue 和 Stack 体系

    目录 Stack Queue Deque 其他特性 BlockingQueue 特点 PriorityQueue 优先级队列 特点 扩容机制 ArrayDeque 继承关系 底层实现 扩容机制 总结 Stack 栈结构类型,表示对象的后进先出堆栈.Stack 继承自 Vector ,并拓展了五个允许将容器视为栈结构的操作. 包括常见的 pop 和 push 操作.以及查看栈顶元素的方法.检查栈是否为空的方法以及从栈顶向下进行搜索某个元素,并获取该元素在栈内深度的方法. Deque 接口及其实现提

  • JAVA集合框架专题

    一.Java集合框架概述 集合可以看作是一种容器,用来存储对象信息.所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下. 数组与集合的区别如下: (1)数组长度不可变化而且无法保存具有映射关系的数据:集合类用于保存数量不确定的数据,以及保存具有映射关系的数据. (2)数组元素既可以是基本类型的值,也可以是对象:集合只能保存对象. Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口:

  • Java集合框架超详细小结

    目录 一:Collection集合 1.1集合概述: 1.2集合架构 1.3Collection集合常用方法 二:迭代器Iterator 2.1Iterator接口 2.2Iterator的实现原理: 2.3增强for() 2.4迭代器注意事项 三:泛型 3.1泛型概述 3.2泛型的优缺点 3.3泛型的定义与使用 泛型方法 泛型接口 3.4泛型的通配符 通配符高级使用-----受限泛型 四:Java常见数据结构 4.1栈 4.2队列 4.3数组 4.4链表 4.5红黑树 五:List集合体系 5

  • 关于Java集合框架面试题(含答案)上

    1.Java集合框架是什么?说出一些集合框架的优点? 每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector.Stack.HashTable和Array.随着集合的广泛使用,Java1.2提出了囊括所有集合接口.实现和算法的集合框架.在保证线程安全的情况下使用泛型和并发集合类,Java已经经历了很久.它还包括在Java并发包中,阻塞接口以及它们的实现.集合框架的部分优点如下: (1)使用核心集合类降低开发成本,而非实现我们自己的集合类. (2)随着使用经过严格测试的集合框架类,代

  • 关于Java集合框架面试题(含答案)下

    21.HashMap和HashTable有何不同? (1)HashMap允许key和value为null,而HashTable不允许. (2)HashTable是同步的,而HashMap不是.所以HashMap适合单线程环境,HashTable适合多线程环境. (3)在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺序,你很容易从HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的. (4)HashMap

  • 关于Java集合框架的总结

    本篇文章先从整体介绍了Java集合框架包含的接口和类,然后总结了集合框架中的一些基本知识和关键点,并结合实例进行简单分析.当我们把一个对象放入集合中后,系统会把所有集合元素都当成Object类的实例进行处理.从JDK1.5以后,这种状态得到了改进:可以使用泛型来限制集合里元素的类型,并让集合记住所有集合元素的类型. 一.综述 所有集合类都位于java.util包下.集合中只能保存对象(保存对象的引用变量).(数组既可以保存基本类型的数据也可以保存对象). 当我们把一个对象放入集合中后,系统会把所

  • Java 集合框架之List 的使用(附小游戏练习)

    目录 1. List 1.1 List 的常见方法 1.2 代码示例 2. ArrayList 2.1 介绍 2.2 ArrayList 的构造方法 2.3 ArrayList 底层数组的大小 3. LinkedList 3.1 介绍 3.2 LinkedList 的构造方法 4. 练习题 5. 扑克牌小游戏 1. List 1.1 List 的常见方法 1.2 代码示例 注意: 下面的示例都是一份代码分开拿出来的,上下其实是有逻辑关系的 示例一: 用 List 构造一个元素为整形的顺序表 Li

  • Java集合框架之List ArrayList LinkedList使用详解刨析

    目录 1. List 1.1 List 的常见方法 1.2 代码示例 2. ArrayList 2.1 介绍 2.2 ArrayList 的构造方法 2.3 ArrayList 底层数组的大小 3. LinkedList 3.1 介绍 3.2 LinkedList 的构造方法 4. 练习题 5. 扑克牌小游戏 1. List 1.1 List 的常见方法 方法 描述 boolean add(E e) 尾插 e void add(int index, E element) 将 e 插入到 inde

  • Java集合框架中迭代器Iterator解析

    Java里面的数组数据可以通过索引来获取,那么对象呢?也是通过索引吗?今天我们就来分析一下Java集合中获取集合对象的方法迭代-Iterator. 本篇文章主要分析一下Java集合框架中的迭代器部分,Iterator,该源码分析基于JDK1.8,分析工具,AndroidStudio,文章分析不足之处,还请指正! 一.简介 我们常常使用 JDK 提供的迭代接口进行 Java 集合的迭代. Iterator iterator = list.iterator(); while(iterator.has

随机推荐