JDK数据结构之ArrayDeque

事情是这样的

在一次开发当中,我想用到队列的相关特性,这次队列不用考虑在并发情况下的安全特性,并发包里面的数据结构就不用考虑了。于是我找啊找,到了jdk中队列的实现有LinkedList,ArrayDeque,PriorityQueue。因为这次使用不需要考虑优先级,首先就排除了PriorityQueue。剩下LinkedList,ArrayDeque了。很纠结啊,到底用谁呢,LinkdList很熟悉,是一个双向链表,对于头部和尾部的增删操作支持都非常好,再看看ArrayDeque,他认识我,我不是很认识它,我就带着好奇的心态去详细的了解这个结构。

看到ArrayDeque 描述说,如果你想把LinkedList当作队列来用,那么ArrayDeque会更快。哈哈,正合我意,为什么这么快呢?让我们来详细了解这个结构吧。首先ArrayDeque 是一个用数组实现的没有容量限制的双端队列。所谓双端队列,是可以在队列的头部和尾部都进行进行如添加和删除操作的队列。ArrayDeque继承了AbstractCollection,实现了Deque。ArrayDeque的增删操作都是围绕head下标和tail下标来对数组进行操作的。对一块连续的内存进行读取,设置某内存的值都是很快的。

优缺点:

1.没有容量限制。

2.多线程环境下不支持并发访问。

3.不支持插入空元素。

4.当把LinkedList 用做queue 的时候,把Stack 用做stack 时,ArrayDeque 速度会比他们更快。

到底有多快,数据说话

当我们分别用ArrayDeque和LinkedList 分别对1百万个数进行入队,出队的操作时,他们所花费的时间:

package java_util_t;
import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.Queue;

public class ArrayDequeDemo {
    
    /**
     * @param args
     */
    public static void main(String[] args) {
        int time = 1000000;
        
        long s1 = System.currentTimeMillis();
        Queue<Integer> queue1 = new ArrayDeque<Integer>(time);
        for (int i = 0; i < time; i++) {
            queue1.offer(i);
        }
        for (int i = 0; i < time; i++) {
            queue1.poll();
        }
        System.out.println("ArrayDeque cost time:" + (System.currentTimeMillis() - s1));
        
        long s2 = System.currentTimeMillis();
        Queue<Integer> queue2 = new LinkedList<Integer>();
        for (int i = 0; i < time; i++) {
            queue2.offer(i);
        }
        for (int i = 0; i < time; i++) {
            queue2.poll();
        }
        System.out.println("LinkedList cost time:" + (System.currentTimeMillis() - s2));
    }

}

源码分析

对于ArrayDeque源码的分析主要从Queue接口的实现来分析,就是说一端入队一端出队的结构来分析。

1.ArrayDeque 的构造

当我们我们初始化一个Queue<Integer> deque = new ArrayDeque<Integer>(6); 时候。会默认选一个>6的最小的2^n。

    public ArrayDeque() {//底层默认大小为16 的Object 数组
        elements = (E[]) new Object[16];
    }

    public ArrayDeque(int numElements) {//构造一个 容量>= numElients 的队列
        allocateElements(numElements);//分配底层数组元素,默认为null
    }

    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) {//这里设计很巧妙,会寻找一个>numElements 的2的n次幂的一个初始容量,如果数值越界了导致出现了负数,就给个2^30
            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];
    }

2.ArrayDeque 入队操作

Queue<Integer> deque = new ArrayDeque<Integer>(6);

deque.offer(1);

当我们执行这个操作的时候:

public boolean offer(E e) {//队列入队操作
    return offerLast(e);//队列尾部入队操作
}
public boolean offerLast(E e) {
    addLast(e);
    return true;
}
public void addLast(E e) {
    if (e == null)//不允许插入Null元素,否则抛出异常
        throw new NullPointerException();
    elements[tail] = e;//设置tail下标元素
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)//你可以把数组队列想象成一个环形数组,当tail+1超过数组最后一个下标时,&操作返回0,此时tail ==head
 doubleCapacity();//扩容
}

private void doubleCapacity() { //入队时,如果队列元素为空,就会扩容
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p;
    int newCapacity = n << 1;//构造或者上一次扩容时队列的容量总是为2^n,当扩充容量为当前容量的2倍时,只需一个左移操作即可。
    if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);//把旧队列head下标到(element.length-1下标)的元素复制到新队列的开头。(1)部分
    System.arraycopy(elements, 0, a, r, p);//复制0~head下标之间的元素到到新队列的(1)部分之后。
    elements = (E[])a; //替换旧的元素数组
    head = 0;//重新设置头结点
    tail = n;//重新设置尾节点
}

扩容图示:

当我们执行以下代码时,会发生扩容

	Queue<Integer> deque = new ArrayDeque<Integer>(6);
		for (int i = 1; i <= 7; i++) {
			deque.offer(i);
		}
		//第(1)部分
		deque.offer(8);//第(2)部分,当执行这串代码的时候会扩容

当程序跑到第(1)部分时,已经入队了7个元素此时,

继续入队8会发生扩容:

deque.offer(8);此时这个操作会判断tail == head 将当前容量设置为双倍容量

3.ArrayDeque 出队操作

我们执行如下操作

Queue<Integer> deque = new ArrayDeque<Integer>(6);

deque.offer(1);

deque.poll();

当出队的时候:

public E poll() {//队列出队
    return pollFirst();//出队第一个元素
}
public E pollFirst() {
    int h = head;
    E result = elements[h]; //出队操作时,会返回第一个元素给调用者
    if (result == null)
        return null;
    elements[h] = null;     //出队操作的实质是通过将队列的头元素设置为null,然后再将head下标往后移动一位。
    head = (h + 1) & (elements.length - 1);//这里就是上一步说的将head下标往后移动一位,
    //而这个head = (h + 1) & (elements.length - 1) 操作保证head在往后移动的时候不会数组越界
    return result;
}

4.ArrayDeque的一些访问操作

public E element() {//查看队列头元素,其实跟peek()唯一不同的是,当队列为空时,element()会抛出NoSuchElementException
    return getFirst();//获取头元素
}
public E getFirst() {
    E x = elements[head];//直接返回数组head下标指向的元素
    if (x == null)
        throw new NoSuchElementException();
    return x;
}
 public E peek() {//查看队列头元素,当队列为空时直接返回null,不抛异常
       return peekFirst();
 }
  public E peekFirst() {
        return elements[head]; // 如果队列为空时,返回elments[head] = null;
  }

5.ArrayDeque 其他的一些操作

public void clear() {//清空操作
    int h = head;
    int t = tail;
    if (h != t) { // head != tail 表示队列元素 不为空
        head = tail = 0;//设置head 和 tail 初始状态
        int i = h;
        int mask = elements.length - 1;
        do {
            elements[i] = null;//配合循环将所有元素设置为null
            i = (i + 1) & mask;
        } while (i != t);
    }
}
public boolean contains(Object o) {//判断队列是否包含该元素
    if (o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    E x;
    while ( (x = elements[i]) != null) {//从head元素向后猪哥判断,是否equals
        if (o.equals(x))
            return true;
        i = (i + 1) & mask;
    }
    return false;
}
  public int size() {//获取队列元素个数,(tail - head) & (elements.length - 1)保证大小在有效范围内。
        return (tail - head) & (elements.length - 1);
    }

  public boolean isEmpty() {//入队操作,tail+=1;出队操作head+=1;当一直出队元素的时候,head一直+,会==tail,此时head==tail都指向null元素。
        return head == tail;
    }
				
时间: 2024-07-28 15:48:05

JDK数据结构之ArrayDeque的相关文章

redis面试总结(一)

1.项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果? 面试题剖析 为什么要用缓存? 用缓存,主要有两个用途:高性能.高并发. 高性能 假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 mysql,半天查出来一个结果,耗时 600ms.但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户.那么此时咋办? 缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql 折腾 600ms

4.redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?

作者:中华石杉 面试题 redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现? 面试官心理分析 如果你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想当然的认为写进 redis 的数据就一定会存在,后面导致系统各种 bug,谁来负责? 常见的有两个问题: 往 redis 写入的数据怎么没了? 可能有同学会遇到,在生产环境的 redis 经常会丢掉一些数据,写进去了,过一会儿可能就没了.我的天,同学,你问这个问题就说明 redis 你就没用对啊.re

给jdk写注释系列之jdk1.6容器(13)-总结篇之Java集合与数据结构

是的,这篇blogs是一个总结篇,最开始的时候我提到过,对于java容器或集合的学习也可以看做是对数据结构的学习与应用.在前面我们分析了很多的java容器,也接触了好多种常用的数据结构,今天我们就来总结下这些内容. 下面我们以数据结构的维度来总结下,在Java集合的实现过程中,底层到底使用了哪些常用的数据结构中,他们分别又有什么特点.      1. 数组(Array) 结构说明:在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来.这些按序排列的同类数据元素的集合称为数组

JDK容器与并发—数据结构

基础数据结构 数组 对于n个元素的数组可如下表示: 数组在初始化时需要明确指定长度,一旦成功完成后,该数组所占用的内存空间就固定下来,一般是连续分配的内存空间.例如对于数组array,可以通过array[i]来访问或设置数组元素值(其中i为索引,对于长度为n的数组,第一个索引值为0,最后一个为n-1). 优点:随机访问效率高,时间复杂度为O(1):缺点:长度固定,不能动态增加.删除元素. 单链表 对于n个节点的单链表可以如下表示: 其类可如下表示: class Node<E> { E item

java 中的JDK封装的数据结构和算法解析(集合类)----链表 List 之 LinkedList

List 只要有两个实现类(ArrayList 和linkedList ),ArryList是基于数组实现,LinkedList是基于链表实现,下面是小弟对LinkedList的一点理解: LinkedList :基于链表实现的集合        双链接列表实现{ @code  List}和{ @code   Deque} 接口.实现了所有可选列表操作,和许可元素(including{ @code  null}). 首先对实现的接口分析一下: Deque  (双端队列): 这种队列允许在队列头和

Queue之ArrayDeque源码

前面讲了Stack是一种先进后出的数据结构:栈,那么对应的Queue是一种先进先出(First In First Out)的数据结构:队列. 对比一下Stack,Queue是一种先进先出的容器,它有两个口,从一个口放入元素,从另一个口获取元素.如果把栈比作一个木桶,那么队列就是一个管道. 是不是很容易理解,因为队列有两个口,一个负责入队另一个负责出队,所以会有先进先出的效果. 当然我们说ArrayDeque是一个双向队列,队列的两个口都可以入队和出队操作.再进一步说,其实ArrayDeque可以

给jdk写注释系列之jdk1.6容器(12)-PriorityQueue源码解析

PriorityQueue是一种什么样的容器呢?看过前面的几个jdk容器分析的话,看到Queue这个单词你一定会,哦~这是一种队列.是的,PriorityQueue是一种队列,但是它又是一种什么样的队列呢?它具有着什么样的特点呢?它的底层实现方式又是怎么样的呢?我们一起来看一下. PriorityQueue其实是一个优先队列,什么是优先队列呢?这和我们前面讲的先进先出(First In First Out )的队列的区别在于,优先队列每次出队的元素都是优先级最高的元素.那么怎么确定哪一个元素的优

JDK源码学习LinkedList

LinkedList是List接口的子类,它底层数据结构是双向循环链表.LinkedList还实现了Deque接口(double-end-queue双端队列,线性collection,支持在两端插入和移除元素).所以LinkedList既可以被当作双向链表,还可以当做栈.队列或双端队列进行操作.文章目录如下: 1.LinkedList的存储实现(jdk 1.7.0_51) 2.LinkedList的读取实现 3.LinkedList的性能分析 下面我们进入正题,开始学习LinkedList. L

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

接口类图 Java Collection由两套并行的接口组成,一套是Collection接口,一套是Map接口.如下图 公共抽象类AbstractCollection 要求派生类实现iterator()方法,AbstractCollection根据得到的Iterator实现所有可以支持的方法,比如remove().contains().toArray().toString()等. 当然,Map系列的类并不从AbstractCollection派生. List实现 AbstractList 要求子