java ArrayDeque详解 学习记录(一)

ArrayDeque是java中对双端队列的线性实现

一.特性

  1. 无容量大小限制,容量按需增长;
  2. 非线程安全队列,无同步策略,不支持多线程安全访问;
  3. 当用作栈时,性能优于Stack,当用于队列时,性能优于LinkedList;
  4. 两端都可以操作;
  5. 具有fail-fast特征;
  6. 不能存储null;
  7. 支持双向迭代器遍历
    ArrayDeque的迭代器和大多数容器迭代器一样,都是快速失败(fail-fast),但是程序不能利用这个特性决定是或否进行了并发操作。

二.实现和继承

ArrayDeque<E> extends AbstractCollection<E>
                         implements Deque<E>, Cloneable, Serializable

在这里插入图片描述

实现的接口

Cloneable、Serializable无接口,Deque接口如下,大致看看,不做详细了解

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);
	boolean add(E e);
	boolean offer(E e);
	E remove();
	E poll();
	E element();
	E peek();
	void push(E e);
	E pop();
	boolean remove(Object o);
	boolean contains(Object o);
	public int size();
	Iterator<E> iterator();
	Iterator<E> descendingIterator();
}

继承AbstractCollection

public abstract class AbstractCollection<E> implements Collection<E>{
	protected AbstractCollection() {}
	public abstract Iterator<E> iterator();
	public abstract int size();
	public boolean isEmpty() {
        return size() == 0;
    }
	public boolean contains(Object o) {
        Iterator<E> it = iterator();
        ...
    }
	public Object[] toArray() {
        Object[] r = new Object[size()];
       ...
    }
	@SuppressWarnings("unchecked")
	public <T> T[] toArray(T[] a) {
    	int size = size();
    	...
	}
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	@SuppressWarnings("unchecked")
	private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
    	int i = r.length;
    	...
	}
	private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError("Required array size too large");
        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    }
	public boolean add(E e) {
        throw new UnsupportedOperationException();
    }
	public boolean remove(Object o) {
        Iterator<E> it = iterator();
        ...
	}
	public boolean containsAll(Collection<?> c) {
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }
	public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }
	public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<?> it = iterator();
        while (it.hasNext()) {
            if (c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }
	public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<E> it = iterator();
        while (it.hasNext()) {
            if (!c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }
	public void clear() {
        Iterator<E> it = iterator();
        while (it.hasNext()) {
            it.next();
            it.remove();
        }
    }
	public String toString() {
        Iterator<E> it = iterator();
        if (! it.hasNext())
            return "[]";

        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (;;) {
            E e = it.next();
            sb.append(e == this ? "(this Collection)" : e);
            if (! it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    }

}

三.源码分析

1.ArrayDeque数据域

	/**
     * 用于存储deque元素的数组。
     * deque的容量是这个数组的长度总是二次方。永远不允许数组变成已满,
     * 除非在addX方法中临时存在在变满后立即调整大小,从而避免了头尾缠绕相等其他。
     * 我们还保证所有阵列单元不保持deque元素始终为null
     */
    transient Object[] elements; // 非私有以简化嵌套类访问
    /**
     * 元素在deque头部的索引(这是将通过remove()或pop()移除的元素);
     * 或者如果deque为空则为等于tail的任意数字。
     */
    transient int head;
    /**
     * 下一个元素将被添加到deque尾部的索引(通过addLast(E)、add(E)或push(E))。
     */
    transient int tail;
    /**
     * 我们将用于新创建的deque的最小容量。必须是2的幂。
     */
    private static final int MIN_INITIAL_CAPACITY = 8;

首先看下ArrayDeque持有的成员域,其中非常核心的是elements,head,tail三个。

  • elements:
    该数组用于存储队列元素,且是大小总是2的幂次方(后面会介绍为什么?)。这个数组不会满容量,会在add方法中扩容,使得头head和tail不会缠绕在一起(即head增长或不会超过tail,head减小时不会溢出到tail),这里队列长度是2的幂次方的原因后续会阐明;
  • head: 双端队列的头位置,出队时或者弹出栈时的元素位置,加入双端队列头端元素位置,表示当前头元素位置;
  • tail:双端队列的尾,入队和进栈时的元素位置,加入双端队列尾端的下个元素的索引,tail位总是空的;
  • MIN_INITIAL_CAPACITY:最小的初始化容量

2.构造函数

	/**
     * 构造一个具有初始容量的空数组deque足以容纳16个元素。
     */
    public ArrayDeque() {
        elements = new Object[16];
    }
    /**
     * 构造一个具有初始容量的空数组deque足以容纳指定数量的元素。
     *@param numElements关于deque初始容量的下限
     */
    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }
    /**
     * 构造一个包含指定的元素的deque集合,按集合的返回顺序迭代器。
     * @param c 要将其元素放入deque中的集合如果指定的集合为null,
     * 则@throws NullPointerException
     */
    public ArrayDeque(Collection<? extends E> c) {
        allocateElements(c.size());
        addAll(c);
    }
  • 第一个默认的无参构造函数:创建初始化大小为16的队列
  • 第二个构造函数:根据参数numElements创建队列,如果numElements小于8,则队列初始化大小为8;如果numElements大于8,则初始化大小为大于numElements的最小2的幂次方。如:numElements=17,则初始化大小为32
  • 第三个构造函数:根据集合元素创建队列,初始化大小为大于集合大小的最小2的幂次方。

3.ArrayDeque(Collection<? extends E> c)构造函数

这里重点看下第二个构造器的过程。其中调用allocateElements(numElements)方法,该方法用来实现容量分配,下面看下内部具体实现:

	public ArrayDeque(Collection<? extends E> c) {
        allocateElements(c.size());
        addAll(c);
    }
    /**
    *分配空数组以容纳给定数量的元素
    */
    private void allocateElements(int numElements) {
        elements = new Object[calculateSize(numElements)];
    }
    /**
    * 首先判断指定大小numElements与MIN_INITIAL_CAPACITY的大小关系。
    * 如果小于MIN_INITIAL_CAPACITY,则直接分配大小为MIN_INITIAL_CAPACITY的数组;
    * 如果大于MIN_INITIAL_CAPACITY,则进行无符号右移操作,然后在加1,
    * 这样就可以寻找到大于numElements的最小2的幂次方。
    */
    private static int calculateSize(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)   // 元素太多,必须后退
                initialCapacity >>>= 1;// 好运分配2^30个元素
        }
        return initialCapacity;
    }
	public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }

首先判断指定大小numElements与MIN_INITIAL_CAPACITY的大小关系。如果小于MIN_INITIAL_CAPACITY,则直接分配大小为MIN_INITIAL_CAPACITY的数组;如果大于MIN_INITIAL_CAPACITY,则进行无符号右移操作,然后在加1,这样就可以寻找到大于numElements的最小2的幂次方。
原理:无符号右移再进行按位或操作,就是将其低位全部补成1,然后再自加加一次,就是再向前进一位。这样就能得到其最小的2次幂。之所以需要最多移16位,是为了能够处理大于2^16次方数。
最后再判断值是否小于0,因为如果初始值在int最大值231-1和230之间,进行一系列移位操作后将得到int最大值,再加1,则溢出变成负数,所以需要检测临界值,然后再右移1位!!!

小知识:>> 表示右移,如果该数为正,则高位补0,若为负数,则高位补1 ; >>> 表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0;

/**
 * 小知识解析 负数在计算机中以补码表示,如果对char、byte、short类型移位,
 * 则在移位前,它们会被转成int类型,且返回值也是int类型;如果对long类型移位,则返回值也是long。
 */ 
// -15原码    10000000 00000000 00000000 00001111 
//    反码    11111111 11111111 11111111 11110000
//    补码    11111111 11111111 11111111 11110001
int bin_0b =0b11111111111111111111111111110001;
System.err.println(bin_0b);//-15

int aa = -15;// 11111111 11111111 11111111 11110001 
//无符号右移两位  00111111 11111111 11111111 11111100 
System.err.println(aa >>>  2); // 1073741820
//右移两位 高位补1   11111111 11111111 11111111 1111100
System.err.println(aa >>  2); // -4
System.err.println(Integer.toBinaryString(-4)); //11111111 11111111 11111111 11111100
// -4 原码    10000000 00000000 00000000 00000100 
//    反码    11111111 11111111 11111111 11111011
//    补码    11111111 11111111 11111111 11111100

以50和1200000000计算示例如下

// 50
		//  11001
		// 110010
		// 111011
		System.err.println("位或第一次:" + 0b111011);
		//   1110
		// 111011
		// 111111
		System.err.println("位或第二次:" + 0b111111);
		//     11
		// 111111
		// 111111
		System.err.println("位或第三次:" + 0b111111);
		// 111111
		System.err.println("位或第四次:" + 0b111111);
		// 111111
		System.err.println("位或第五次:" + 0b111111);
		// 111111
		// 1000000
		System.err.println("加1:" + 0b1000000); //64
		System.err.println(calculateSize(50)); //64
// 1200000000
		//  100011110000110100011000000000
		// 1000111100001101000110000000000
		// 1100111110001111100111000000000
		System.err.println("位或第一次:" + 0b1100111110001111100111000000000);
		//   11001111100011111001110000000
		// 1100111110001111100111000000000
		// 1111111111101111111111110000000
		System.err.println("位或第二次:" + 0b1111111111101111111111110000000);
		//     111111111110111111111111000
		// 1111111111101111111111110000000
		// 1111111111111111111111111111000
		System.err.println("位或第三次:" + 0b1111111111111111111111111111000);
		//         11111111111111111111111
		// 1111111111111111111111111111000
		// 1111111111111111111111111111111
		System.err.println("位或第四次:" + 0b1111111111111111111111111111111);
		//                 111111111111111
		// 1111111111111111111111111111111
		System.err.println("位或第五次:" + 0b1111111111111111111111111111111);
		//  1111111111111111111111111111111
		// 10000000000000000000000000000000
		System.err.println("加1:" + 0b10000000000000000000000000000000);

4.重要行为

addFirst方法

	/**
     * 在此deque的前面插入指定的元素.
     * @param e 要添加的元素
     * 如果指定的元素为null,则引发NullPointerException
     */
    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
    }

首先判断插入元素是否为空,再计算即将插入的位置,计算出后将元素赋值给相应的槽位,最后再判断队列容量进行扩容。

  1. 将数组的高位端作为双端队列的头部,将低位作为双端队列尾部。每从头部加入一个元素时,head头逆时针向tail尾方向移动一个位置,实现上即将head减1后对数组的最大下标按位与运算。这里就利用了2的幂次方的特性,队列容量设置为2的幂次方后,数组的最大下标位置等于2的幂次方减1,在二进制表示时,就是所有二进制位都是1。这样head位置减1后与其进行按位与运算就能得到头部插入的位置。
  2. 当head等于tail时,就表示队列已经满了。这时需要进行扩容。

再来看下扩容策略:

/**
 *使这个deque的容量加倍. 仅在满时呼叫, 当头部和尾部缠绕在一起变得相等时
 */
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;
}
  1. 按照2倍方式扩容
  2. 扩容后,将原队列中从头部插入的元素即head右边元素从扩容后新数组的0位置开始排放,然后将左边的元素紧接着排放进新数组。
  3. 将head置0,tail置成扩容前数组长度。

addLast方法

	/**
     * 在该deque的末尾插入指定的元素.
     * 此方法等效于{@link#add}
     * @param e 添加的元素
     * @throws 如果指定的元素为null,则引发NullPointerException
     */
    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }

上述的addFirst是逆时针的插入方式,addLast刚好与其相反,即顺时针方向插入,且tail表示的是下一个插入的元素的位置。

  1. 判断元素是否为空,然后直接将元素插入tail槽位
  2. 然后tail向后移动一位,再按位与(控制循环)作为新的tail槽位
  3. 判断新的tail槽位是否与head相等,然后依此进行扩容(这里扩容与上述扩容过程一样,不再赘述)。

pollFirst方法

	public E pollFirst() {
        int h = head;
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result == null)
            return null;
        elements[h] = null;     // Must null out slot
        head = (h + 1) & (elements.length - 1);
        return result;
    }
  1. 取出头元素,如果头元素为空,则返回null
  2. 否则,将头元素槽位置为空(因为pollFirst是移除操作)
  3. 再将head顺时针向后移动一位,即加1再和数组最大下标按位与计算出新的head

读到这里,相信读者已经已经对双端队列的数据结构已经非常清晰,即双端操作的数组,tail向前(顺时针)移动即从尾端插入元素或者向后移动即从尾端移除元素,head向后(逆时针)移动即从头端插入元素或者向前移动即从头端移除元素。这几个过程正好具有FIFO和LIFO的特点,所以ArrayDeque既可以作为队列Queue又可以作为栈Stack。

pollLast方法

	public E pollLast() {
        int t = (tail - 1) & (elements.length - 1);
        @SuppressWarnings("unchecked")
        E result = (E) elements[t];
        if (result == null)
            return null;
        elements[t] = null;
        tail = t;
        return result;
    }

从以上描述的ArrayDeque的数据结构和tail的含义中,可以大致思考下,从尾端移除元素的过程。

  1. 先将tail向后(逆时针)移动一位,然后对数组最大下标按位与计算出将要移除元素的槽位
  2. 取出计算出的槽位中元素,判断是否为空,为空则返回null
  3. 如果不为空,则将该槽位置为空,将槽位下标作为新的tail

双向队列操作

插入元素

addFirst(): 向队头插入元素,如果元素为空,则发生NPE

addLast(): 向队尾插入元素,如果为空,则发生NPE

offerFirst(): 向队头插入元素,如果插入成功返回true,否则返回false

offerLast(): 向队尾插入元素,如果插入成功返回true,否则返回false

移除元素

removeFirst(): 返回并移除队头元素,如果该元素是null,则发生NoSuchElementException

removeLast(): 返回并移除队尾元素,如果该元素是null,则发生NoSuchElementException

pollFirst(): 返回并移除队头元素,如果队列无元素,则返回null

pollLast(): 返回并移除队尾元素,如果队列无元素,则返回null

获取元素

getFirst(): 获取队头元素但不移除,如果队列无元素,则发生NoSuchElementException

getLast(): 获取队尾元素但不移除,如果队列无元素,则发生NoSuchElementException

peekFirst(): 获取队头元素但不移除,如果队列无元素,则返回null

peekLast(): 获取队尾元素但不移除,如果队列无元素,则返回null

栈操作

pop(): 弹出栈中元素,也就是返回并移除队头元素,等价于removeFirst(),如果队列无元素,则发生NoSuchElementException

push(): 向栈中压入元素,也就是向队头增加元素,等价于addFirst(),如果元素为null,则发生NPE,如果栈空间受到限制,则发生IllegalStateException

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值