java集合 ArrayDeque源码详细分析

 更新时间:2019年05月24日 11:07:01   作者:彤哥读源码  
ArrayDeque是一种以数组方式实现的双端队列,它是非线程安全的。下面小编和大家一起学习一下

问题

(1)什么是双端队列?

(2)ArrayDeque是怎么实现双端队列的?

(3)ArrayDeque是线程安全的吗?

(4)ArrayDeque是有界的吗?

简介

双端队列是一种特殊的队列,它的两端都可以进出元素,故而得名双端队列。

ArrayDeque是一种以数组方式实现的双端队列,它是非线程安全的。

继承体系

通过继承体系可以看,ArrayDeque实现了Deque接口,Deque接口继承自Queue接口,它是对Queue的一种增强。

public interface Deque<E> extends Queue<E> {
 // 添加元素到队列头
 void addFirst(E e);
 // 添加元素到队列尾
 void addLast(E e);
 // 添加元素到队列头
 boolean offerFirst(E e);
 // 添加元素到队列尾
 boolean offerLast(E e);
 // 从队列头移除元素
 E removeFirst();
 // 从队列尾移除元素
 E removeLast();
 // 从队列头移除元素
 E pollFirst();
 // 从队列尾移除元素
 E pollLast();
 // 查看队列头元素
 E getFirst();
 // 查看队列尾元素
 E getLast();
 // 查看队列头元素
 E peekFirst();
 // 查看队列尾元素
 E peekLast();
 // 从队列头向后遍历移除指定元素
 boolean removeFirstOccurrence(Object o);
 // 从队列尾向前遍历移除指定元素
 boolean removeLastOccurrence(Object o);
// *** 队列中的方法 ***
 // 添加元素,等于addLast(e)
 boolean add(E e);
 // 添加元素,等于offerLast(e)
 boolean offer(E e);
 // 移除元素,等于removeFirst()
 E remove();
 // 移除元素,等于pollFirst()
 E poll();
 // 查看元素,等于getFirst()
 E element();
 // 查看元素,等于peekFirst()
 E peek();
// *** 栈方法 ***
// 入栈,等于addFirst(e)
 void push(E e);
 // 出栈,等于removeFirst()
 E pop();
// *** Collection中的方法 ***
 // 删除指定元素,等于removeFirstOccurrence(o)
 boolean remove(Object o);
 // 检查是否包含某个元素
 boolean contains(Object o);
 // 元素个数
 public int size();
 // 迭代器
 Iterator<E> iterator();
 // 反向迭代器
 Iterator<E> descendingIterator();
}

Deque中新增了以下几类方法:

(1)*First,表示从队列头操作元素;

(2)*Last,表示从队列尾操作元素;

(3)push(e),pop(),以栈的方式操作元素的方法;

源码分析

主要属性

// 存储元素的数组
transient Object[] elements; // non-private to simplify nested class access
// 队列头位置
transient int head;
// 队列尾位置
transient int tail;
// 最小初始容量
private static final int MIN_INITIAL_CAPACITY = 8;

从属性我们可以看到,ArrayDeque使用数组存储元素,并使用头尾指针标识队列的头和尾,其最小容量是8。

主要构造方法

// 默认构造方法,初始容量为16
public ArrayDeque() {
 elements = new Object[16];
}
// 指定元素个数初始化
public ArrayDeque(int numElements) {
 allocateElements(numElements);
}
// 将集合c中的元素初始化到数组中
public ArrayDeque(Collection<? extends E> c) {
 allocateElements(c.size());
 addAll(c);
}
// 初始化数组
private void allocateElements(int numElements) {
 elements = new Object[calculateSize(numElements)];
}
// 计算容量,这段代码的逻辑是算出大于numElements的最接近的2的n次方且不小于8
// 比如,3算出来是8,9算出来是16,33算出来是64
private static int calculateSize(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
 }
 return initialCapacity;
}

通过构造方法,我们知道默认初始容量是16,最小容量是8。

入队

入队有很多方法,我们这里主要分析两个,addFirst(e)和addLast(e)。

// 从队列头入队
public void addFirst(E e) {
 // 不允许null元素
 if (e == null)
 throw new NullPointerException();
 // 将head指针减1并与数组长度减1取模
 // 这是为了防止数组到头了边界溢出
 // 如果到头了就从尾再向前
 // 相当于循环利用数组
 elements[head = (head - 1) & (elements.length - 1)] = e;
 // 如果头尾挨在一起了,就扩容
 // 扩容规则也很简单,直接两倍
 if (head == tail)
 doubleCapacity();
}
// 从队列尾入队
public void addLast(E e) {
 // 不允许null元素
 if (e == null)
 throw new NullPointerException();
 // 在尾指针的位置放入元素
 // 可以看到tail指针指向的是队列最后一个元素的下一个位置
 elements[tail] = e;
 // tail指针加1,如果到数组尾了就从头开始
 if ( (tail = (tail + 1) & (elements.length - 1)) == head)
 doubleCapacity();
}

(1)入队有两种方式,从队列头或者从队列尾;

(2)如果容量不够了,直接扩大为两倍;

(3)通过取模的方式让头尾指针在数组范围内循环;

(4)x & (len - 1) = x % len,使用&的方式更快;

扩容

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];
 // 将旧数组head之后的元素拷贝到新数组中
 System.arraycopy(elements, p, a, 0, r);
 // 将旧数组下标0到head之间的元素拷贝到新数组中
 System.arraycopy(elements, 0, a, r, p);
 // 赋值为新数组
 elements = a;
 // head指向0,tail指向旧数组长度表示的位置
 head = 0;
 tail = n;
}

扩容这里迁移元素可能有点绕,请看下面这张图来理解。

出队

出队同样有很多方法,我们主要看两个,pollFirst()和pollLast()。

// 从队列头出队
public E pollFirst() {
 int h = head;
 @SuppressWarnings("unchecked")
 // 取队列头元素
 E result = (E) elements[h];
 // 如果队列为空,就返回null
 if (result == null)
 return null;
 // 将队列头置为空
 elements[h] = null; // Must null out slot
 // 队列头指针右移一位
 head = (h + 1) & (elements.length - 1);
 // 返回取得的元素
 return result;
}
// 从队列尾出队
public E pollLast() {
 // 尾指针左移一位
 int t = (tail - 1) & (elements.length - 1);
 @SuppressWarnings("unchecked")
 // 取当前尾指针处元素
 E result = (E) elements[t];
 // 如果队列为空返回null
 if (result == null)
 return null;
 // 将当前尾指针处置为空
 elements[t] = null;
 // tail指向新的尾指针处
 tail = t;
 // 返回取得的元素
 return result;
}

(1)出队有两种方式,从队列头或者从队列尾;

(2)通过取模的方式让头尾指针在数组范围内循环;

(3)出队之后没有缩容哈哈^^


前面我们介绍Deque的时候说过,Deque可以直接作为栈来使用,那么ArrayDeque是怎么实现的呢?

public void push(E e) {
 addFirst(e);
}

public E pop() {
 return removeFirst();
}

是不是很简单,入栈出栈只要都操作队列头就可以了。

总结

(1)ArrayDeque是采用数组方式实现的双端队列;

(2)ArrayDeque的出队入队是通过头尾指针循环利用数组实现的;

(3)ArrayDeque容量不足时是会扩容的,每次扩容容量增加一倍;

(4)ArrayDeque可以直接作为栈使用;

彩蛋

双端队列与双重队列?

双端队列(Deque)是指队列的两端都可以进出元素的队列,里面存储的是实实在在的元素。

双重队列(Dual Queue)是指一种队列有两种用途,里面的节点分为数据节点和非数据节点,它是LinkedTransferQueue使用的数据结构。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • mybatis Map查询结果下划线转驼峰的实例

    mybatis Map查询结果下划线转驼峰的实例

    这篇文章主要介绍了mybatis Map查询结果下划线转驼峰的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • JAVA中的 map,list,set

    JAVA中的 map,list,set

    这篇文章主要介绍了JAVA中的 map、list、set这三个集合,下面文章围绕JAVA中的三大集合 map、list、set详情讲解,需要的朋友可以参考一下
    2021-11-11
  • 一文掌握maven  filtering标签

    一文掌握maven  filtering标签

    这篇文章主要介绍了maven  filtering标签,本文通过三种方法给大家讲解maven filtering标签,结合示例代码给大家介绍的非常详细,需要的朋友可以参考下
    2023-02-02
  • Java设计模式之外观模式

    Java设计模式之外观模式

    这篇文章介绍了Java设计模式之外观模式,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • Java经典面试题汇总:Spring

    Java经典面试题汇总:Spring

    本篇总结的是Spring框架相关的面试题,后续会持续更新,希望我的分享可以帮助到正在备战面试的实习生或者已经工作的同行,如果发现错误还望大家多多包涵,不吝赐教,谢谢
    2021-07-07
  • 深入了解Java中Synchronized的各种使用方法

    深入了解Java中Synchronized的各种使用方法

    在Java当中synchronized关键字通常是用来标记一个方法或者代码块。本文将通过示例为大家详细介绍一下Synchronized的各种使用方法,需要的可以参考一下
    2022-08-08
  • MybatisPlus使用@TableId主键id自增长无效的解决

    MybatisPlus使用@TableId主键id自增长无效的解决

    本文主要介绍了MybatisPlus使用@TableId主键id自增长无效的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • java报错:“错误:编码GBK 的不可映射字符”解决办法

    java报错:“错误:编码GBK 的不可映射字符”解决办法

    当Java源代码中包含中文字符时,我们在用javac编译时会出现“错误:编码GBK的不可映射字符”,这篇文章主要给大家介绍了关于java报错:“错误:编码GBK 的不可映射字符”的解决办法,需要的朋友可以参考下
    2024-08-08
  • java背包问题动态规划算法分析

    java背包问题动态规划算法分析

    这篇文章主要介绍了java背包问题动态规划算法分析,想了解算法的同学一定要看一下
    2021-04-04
  • 关于idea2022.2 闪退的问题

    关于idea2022.2 闪退的问题

    最近更新了idea2022.2版本,这是一个比较大的软件版本更迭,下面小编给大家介绍下idea2022.2 闪退的问题及解决方法,需要的朋友可以参考下
    2022-08-08

最新评论