Java queue-deque-ArrayDeque源码解析

一、概述

1、在Java中Queue是一个接口,而对于Queue的具体实现由的是采用数组的形式,有的是链表的形式;

2、Queue实现了先进先出(FIFO)的队列的约定;

3、Deque支持在两端插入和删除元素,他是一个双端队列;

二、Queue中的接口定义

Queue接口继承了Collection接口,实现的是先进先出的队列;

接口方法 接口定义 区别
boolean add(E e) 向队列尾部插入元素 如果队列满不允许插入时,抛出异常
boolean offer(E e) 向队列尾部插入元素 如果队列满不允许插入时,返回fase
E remove() 移除一个元素 如果队列为空,抛出异常
E poll() 移除一个元素 如果队列为空,返回null
E element() 获取队列头部元素 如果队列为空,抛出异常
E peek() 获取队列头部元素 如果队列为空,返回null

三、Deque中的接口定义

Deque接口继承了Queue接口,他是一个双端队列;

接口方法 接口定义 区别

void addFirst(E e)

向队头中插入元素 当队满不允许插入时抛出异常
void addLast(E e) 向队尾中插入元素 当队满不允许插入时抛出异常
boolean offerFirst(E e) 向队头中插入元素 当队满不允许插入时返回false
boolean offerLast(E e)
向队尾中插入元素

当队满不允许插入时返回false

E removeFirst() 移除队头的元素 队列为空抛出异常
E removeLast() 移除队尾的元素 队列为空抛出异常
E pollFirst() 移除队头的元素 队列为空返回null
E pollLast() 移除队尾的元素 队列为空返回null
E getFirst() 获取队头元素 队列为空抛出异常
E getLast() 获取队尾元素 队列为空抛出异常
E peekFirst() 获取队头元素 队列为空返回null
E peekLast() 获取队尾元素 队列为空返回null
boolean removeFirstOccurrence(Object o) 从此双端队列移除第一次出现的指定元素  
boolean removeLastOccurrence(Object o) 从此双端队列移除最后一次出现的指定元素  
boolean add(E e) 插入元素到队列尾部 重写queue中的方法
boolean offer(E e) 插入元素到队列尾部 重写queue中的方法
E remove() 删除队列第一个元素 重写queue中的方
E poll() 删除队列第一个元素 重写queue中的方法
E element() 获取队列头部第一个元素 重写queue中的方法
E peek() 获取队列头部第一个元素 重写queue中的方法
void push(E e) 向队列头部插入一个元素 队满不允许插入抛出异常,模仿栈的操作
E pop() 从队列头部取出一个元素 队列为空抛出异常,模仿栈的操作
boolean remove(Object o) s双端队列移除元素 重定义Collection接口中的方法
boolean contaions(Object  o) 判断双端队列是否包含元素 重定义Collection接口中的方法
int size(); 返回双端队列元素个数 重定义Collection接口中的方法

Iterator iterator()

Iterator descendingIterator()

返回双端队列迭代器

返回一个迭代器在此deque队列的顺序相反的元素

重定义Collection接口中的方法

三、ArrayDeque的分析

    在Queue中LinkedList实现了queue的方法,所以可以将LinkedList当做queue来使用,并且在官方推荐中建议使用ArrayDeque,在前面也已经讲过LinkedList,所以在后面将对ArrayDeque进行分析。

    从名字上看可以看出ArrayDeque是通过数组来实现的,但是由于双端队列的特性,该数组必须是循环的,并且队列的头部和尾部是动态的,数组的任何一点都可以被当做是队列的起点或者终点,也就是数组的下标0不一定是代表队列的头部。

    下面是三种情况的队列,可以看到队头位置head不一定永远在队尾tail的前面,他们的索引值的大小是没有比较意义的。

Java queue-deque-ArrayDeque源码解析_第1张图片

1、ArrayDeque的初始化

    在下面可以看出ArrayDeque是用数组存储元素的,其中有两个变量head和tail分别标记队列的头部位置和尾部位置,队列的最小长度是8,初始化默认的的队列长度是16,并且队列的长度必须是2的幂数,即使指定了队列的长度,也必须进行调整,使其为2的次幂,这里为什么必须是2的次幂,在后面在进行说明。

//底层数据用数组存储
transient Object[] elements;

//队列的头部位置,当删除或出栈时会操作该位置的元素
transient int head;

//队列的尾部位置,当插入元素时会使用到该值
transient int tail;

//初始化最小队列长度,必须是2的幂数
private static final int MIN_INITIAL_CAPACITY = 8;

/**
* 初始化一个ArrayDeque,默认容量是16
*/
public ArrayDeque() {
	elements = new Object[16];
}

/**
* 初始化指定容量的ArrayDeque
* @param numElements
*/
public ArrayDeque(int numElements) {
	allocateElements(numElements);
}


/**
* 初始化包含指定集合的ArrayDeque,
* 首先根据集合中元素的个数确定初始化数组的长度,然后将集合中的元素添加到ArrayDeque中
* @param c
*/
public ArrayDeque(Collection c) {
	allocateElements(c.size());
	addAll(c);
}

/**
* 根据指定的容量进行调整,使ArrayDeque的容量必须是2的幂数
*/
private void allocateElements(int numElements) {
	int initialCapacity = MIN_INITIAL_CAPACITY;

	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 = new Object[initialCapacity];
}

2、ArrayDeque的扩容

    ArrayDeque的扩容其实是申请一个更大容量的数组,将原数组中的元素复制到新的数组中,这段代码的实现逻辑是申请一个新的数组,其容量是原数组的2倍,然后做两步复制操作,首先将原数组head右边的元素复制到新的数组中,从0开始位置,然后将原数组head右边的元素复制到新的数组中,最后重新给head和tail赋值,此时head=0,tail为原数组的长度值n。具体的过程和原因可以看下边的图。

private void doubleCapacity() {
	assert head == tail;  //表示队列满了,需要进行扩容
	int p = head;  //p指定为head的位置
	int n = elements.length;   //n为队列的长度
	int r = n - p; //r为p到数组尾间建个的位置个数
	int newCapacity = n << 1;  //新的队列的长度=老队列的长度*2
	if (newCapacity < 0)   //表示超过了队列的最大限制长度
		throw new IllegalStateException("Sorry, deque too big");
	Object[] a = new Object[newCapacity];
	System.arraycopy(elements, p, a, 0, r);   //将旧的数组中p位置右边的元素复制到新的数组中,从0开始的位置
	System.arraycopy(elements, 0, a, r, p);  //将就的数组中0开始的位置右边的元素复制到新的数组中,从r开始的位置
	elements = a;
	head = 0;
	tail = n;
}

Java queue-deque-ArrayDeque源码解析_第2张图片

3、ArrayDeque头部插入元素

    向ArrayDeque的头部插入元素有两种方法,其实现过程是一样的,只是对于满队列情况下返回值的处理不一样。

    向队列的头部插入元素,实际上就是在head的前面插入元素,通常情况下在前面插入元素只需要是element[--head]=e就可以,但是由于ArrayDeque是双端队列,循环数组,会存在数组下标越界的情况,所以在插入时还需要进行下标的检查,因此在插入一个元素值时,存在下面几步校验:

    (1)插入的元素是否为空,在ArrayDeque中不允许存在null元素;

    (2)数组的下标是否越界,由于是循环数组,根据位比较可以很好的找到head的前一个元素的位置;

    (3)空间是否够用,在插入元素之后,队列是否是满队列,如果是满队列,需要进行扩容。

注意:可以看到在检查空间是否够用总是在插入元素之后进行检查的,也就是说tail指向的位置其实是空的,也就是最后一个元素的后一个位置,因此队列中至少都是存在一个空位置的,所以在插入之前不需要考虑空间的问题。

/**
* 向队列头部插入一个元素
* 元素为空抛出异常
* @param e
*/
public void addFirst(E e) {
	//元素为空,抛出异常
	if (e == null)
		throw new NullPointerException();
	//检查下标是否越界
	elements[head = (head - 1) & (elements.length - 1)] = e;
	//空间是否够用,如果头部索引位置等于尾部索引位置,说明队列满了,需要扩容
	if (head == tail)
		doubleCapacity();
}

/**
*队列头部插入元素,实际上是调用addFirst方法
* @param e
* @return
*/
public boolean offerFirst(E e) {
	addFirst(e);
	return true;
}

Java queue-deque-ArrayDeque源码解析_第3张图片

4、ArrayDeque尾部插入元素

    向队列尾部插入元素时,也就是在tail的位置插入元素,由于在前面已经说过tail位置指向的是下一个可以插入的空位,所以在尾部插入时,可以直接在tail位置进行插入,插入完成之后在进行空间校验。

/**
* 向队列尾部插入一个元素
* @param e
*/
public void addLast(E e) {
	//元素为空,抛出异常
	if (e == null)
		throw new NullPointerException();
	//在tail位置插入元素
	elements[tail] = e;
        //获取tail的位置,并判断是否越界,如果队列满了进行扩容
	if ( (tail = (tail + 1) & (elements.length - 1)) == head)
		doubleCapacity();
}
/**
* 队列尾部插入元素
* @param e
* @return
*/
public boolean offerLast(E e) {
	addLast(e);
	return true;
}

Java queue-deque-ArrayDeque源码解析_第4张图片

5、ArrayDeque中的remove和peek等操作

    下面几个方法是删除队头队尾元素或者是取出队头队尾的元素。

    对于队头的操作,removeFirst和pollFirst是取出队头的元素,并删除该位置的元素,因此在取出元素后,需要将head向后移动一位,这时候需要对下标进行判断是否越界;getFirst和peekFirst是获取队头的元素,但不对队列进行操作,所以直接取出head的元素即可。

    对于队尾的操作,removeLast和pollLast是取出队尾的元素,并删除该位置的元素,由于tail总是指向下一个待插入元素的位置,所以需要先获取到tail前一个位置的下标,取出元素,并将该位置元素置空;getLast和peekLast是获取队尾的元素,不对队列进行操作。

//删除队列头部第一个元素
public E removeFirst() {
	E x = pollFirst();
	if (x == null)
		throw new NoSuchElementException();
	return x;
}

//移除队列头部元素
public E pollFirst() {
	int h = head;
	//获取头部元素
	@SuppressWarnings("unchecked")
	E result = (E) elements[h];
	//如果队列为空,返回null
	if (result == null)
		return null;
	//将head位置的元素置为空
	elements[h] = null;
	//head向后移动一位
	head = (h + 1) & (elements.length - 1);
	return result;
}

//删除队列尾部元素
public E removeLast() {
	E x = pollLast();
	if (x == null)
		throw new NoSuchElementException();
	return x;
}

//移除队列尾部元素
public E pollLast() {
	//获取tail前面一个位置
	int t = (tail - 1) & (elements.length - 1);
	@SuppressWarnings("unchecked")
	E result = (E) elements[t];
	if (result == null)
		return null;
	//将该位置元素置为空,表示删除了
	elements[t] = null;
	//tail向前移动一位
	tail = t;
	return result;
}

//获取队列头部元素,为空抛出异常
public E getFirst() {
	@SuppressWarnings("unchecked")
	E result = (E) elements[head];
	if (result == null)
		throw new NoSuchElementException();
	return result;
}

//获取队列尾部元素,为空抛出异常
public E getLast() {
	@SuppressWarnings("unchecked")
	E result = (E) elements[(tail - 1) & (elements.length - 1)];
	if (result == null)
		throw new NoSuchElementException();
	return result;
}

// 获取队列头部元素,为空返回null
@SuppressWarnings("unchecked")
public E peekFirst() {
	return (E) elements[head];
}

//获取队列尾部元素,为空返回null
@SuppressWarnings("unchecked")
public E peekLast() {
	return (E) elements[(tail - 1) & (elements.length - 1)];
}

6、ArrayDeque的delete操作

    delete操作基本上是以少移动数据为原则,也就是删除的位置i如果离head近,那么就移动head到i之间的数据,如果i离tail比较近,就移动i到tail之间的数据,具体的移动步骤和方法可以结合下面的源码和图片进行理解。

private boolean delete(int i) {
	checkInvariants();
	final Object[] elements = this.elements;
	//数组的长度
	final int mask = elements.length - 1;
	final int h = head;
	final int t = tail;
	//i与队列头部的距离
        final int front = (i - h) & mask;
	//i与队列尾部的距离
	final int back  = (t - i) & mask;

	//判断i是否在head和tail之间
	if (front >= ((t - h) & mask))
		throw new ConcurrentModificationException();

	       //i的位置靠近队列头部,那么移动头部的元素,返回false
	      if (front < back) {
		   if (h <= i) {   //如果head是在i的前面,那么直接将h到i之间的元素全部向后移动一位,h位置的元素置空,head向后移动一位
		   System.arraycopy(elements, h, elements, h + 1, front);
	      } else {  //如果h的位置在i的后边,那么需要进行两次复制
		   System.arraycopy(elements, 0, elements, 1, i);
		   elements[0] = elements[mask];
		   System.arraycopy(elements, h, elements, h + 1, mask - h);
	      }
	      elements[h] = null;
	      head = (h + 1) & mask;
	      return false;
        } else {   //i的位置靠近队列的尾部
	      if (i < t) { //如果i在tail的前面
			System.arraycopy(elements, i + 1, elements, i, back);
			tail = t - 1;
		} else { //如果i在tail的后边
			System.arraycopy(elements, i + 1, elements, i, mask - i);
			elements[mask] = elements[0];
			System.arraycopy(elements, 1, elements, 0, t);
			tail = (t - 1) & mask;
		}
		return true;
	}
}


Java queue-deque-ArrayDeque源码解析_第5张图片

Java queue-deque-ArrayDeque源码解析_第6张图片

7、ArrayDeque的队列长度为什么必须是2的次幂?

    在前面的方法中也可以看到,在进行队列的操作时,获取下标的位置都要进行位的与运算,并且是将下标与(队列的长度-1)进行与运算,当队列的长度是2的次幂时,那么减1之后,其二进制为所有的位置都是1,进行与运算时,也就能保证得到一个正确的期望的位置,而位操作是循环数组中一种常见的操作,效率也很高。


四、队列的总结

1、Queue队列是一种特殊的线性表,它允许在队列的前端进行删除操作,在队列的后端进行插入操作 ,队列中最先插入的元素将是最先被删除的元素,因此队列又被成为先进先出的线性表;

2、Deque是一种双端队列,它允许在队列的两端进行操作;

3、ArrayDeque是一种以数组形式实现的双端队列,他的默认容量是16,且其容量必须是2的次幂;

4、ArrayDeque并不是固定大小的队列,每次队列满了就将队列容量扩大一倍,因此总是能成功的插入一个元素;

5、ArrayDeque中不可以存储null元素,因为系统是根据某个位置是否为null来判断元素的存在;

6、ArrayDeque作为栈使用时,性能比Stack好,作为队列使用时,性能比LinkedList好;

7、ArrayDeque不是线程安全的;


你可能感兴趣的:(java)