为什么Deque(ArrayDeque)的容量是2的幂?

时间:2019-06-26 19:21:43

标签: java deque capacity arraydeque

在Java中(但在PHP中类似),ArrayDeque实现的能力始终为2的幂:

http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/ArrayDeque.java#l126

对于HashMap,此选择很明显-基于修剪的32位哈希值具有均匀的元素分布。但是Deque会顺序插入/删除元素。

此外,ArrayList并不将容量限制为2的幂,只是确保其至少为元素数量。

因此,为什么Deque实现要求其容量必须为2的幂次方

4 个答案:

答案 0 :(得分:5)

我猜是出于性能原因。例如,让我们看一下addLast函数的实现:

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

因此,可以写tail = (tail + 1) % elements.length代替tail = (tail + 1) & (elements.length - 1)&的运行速度比%快)。 ArrayDeque的源代码中多次使用了这种构造。

答案 1 :(得分:3)

2的幂可用于某些掩蔽操作。例如,从整数中获取低位位数。

因此,如果大小为64,则64-1为63,即二进制111111。

这有助于在双端队列中定位或放置元素。

答案 2 :(得分:3)

好问题。

查看代码:

  1. 正如您所说,容量始终是2的幂。此外,决不允许双端队列达到容量。

    public class ArrayDeque<E> extends AbstractCollection<E>
                               implements Deque<E>, Cloneable, Serializable
    {
        /**
         * The array in which the elements of the deque are stored.
         * The capacity of the deque is the length of this array, which is
         * always a power of two. The array is never allowed to become
         * full, except transiently within an addX method where it is
         * resized (see doubleCapacity) immediately upon becoming full,
         * thus avoiding head and tail wrapping around to equal each
         * other....
    
  2. “ 2的幂”约定简化了“初始大小”:

     /**
      * Allocates empty array to hold the given number of elements.
      *
      * @param numElements  the number of elements to hold
      */
     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) {
             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
         }
    
  3. 最后,请注意“ mask”的使用:

     /**
      * Removes the last occurrence of the specified element in this
      * deque (when traversing the deque from head to tail).
      * If the deque does not contain the element, it is unchanged.
      * More formally, removes the last element {@code e} such that
      * {@code o.equals(e)} (if such an element exists).
      * Returns {@code true} if this deque contained the specified element
      * (or equivalently, if this deque changed as a result of the call).
      *
      * @param o element to be removed from this deque, if present
      * @return {@code true} if the deque contained the specified element
      */
     public boolean removeLastOccurrence(Object o) {
         if (o == null)
             return false;
         int mask = elements.length - 1;
         int i = (tail - 1) & mask;
         Object x;
         while ( (x = elements[i]) != null) {
             if (o.equals(x)) {
                 delete(i);
                 return true;
             }
             i = (i - 1) & mask;
         }
         return false;
     }
    
     private boolean delete(int i) {
         checkInvariants();
         ...
         // Invariant: head <= i < tail mod circularity
         if (front >= ((t - h) & mask))
             throw new ConcurrentModificationException();
         ...
         // Optimize for least element motion
         if (front < back) {
             if (h <= i) {
                 System.arraycopy(elements, h, elements, h + 1, front);
             } else { // Wrap around
                 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;
    

答案 3 :(得分:3)

最后我找到了!!! 原因不仅在于性能和位掩码操作(是的,它们速度更快,但不明显)。真正的原因是允许环回 elements的容量(如果我们使用顺序的添加/删除操作)。换句话说:在remove()操作之后重新使用释放的单元格。

请考虑以下示例(初始容量为 16 ):

  1. add()

    1. 添加15个元素=> head = 0,tail = 15

    2. 添加5个元素=> doubleCapacity() =>头= 0,尾= 20,容量= 32

    enter image description here

  2. add()-remove()-add()

    1. 添加15个元素=> head = 0,tail = 15

    2. 删除 10个元素=> tail 向后循环以删除索引=> head = 10,tail = 15

    3. 添加更多5个元素=>容量仍然为16,elements[]数组未重建或重新分配! =>将新元素添加到已删除元素的位置到数组的开头=> head = 10,tail = 4(循环从15-> 0-返回数组的开头> 1-> 2-> 3-> 4)。注意,将值16-19插入到索引0-3

    enter image description here

因此,在这种情况下,使用2的幂和简洁的位运算会更有意义。通过这种方法,if ( (tail = (tail + 1) & (elements.length - 1)) == head)之类的操作可以轻松分配并验证 looped tailhead不重叠(是的,实际上是愚蠢的蛇尾巴咬住头部:))

要播放的代码段:

ArrayDeque<String> q = new ArrayDeque<>(15); // capacity is 16

// add 15 elements
q.add("0"); q.add("1"); q.add("2"); q.add("3"); q.add("4");
q.add("5"); q.add("6"); q.add("7"); q.add("8"); q.add("9");
q.add("10"); q.add("11");q.add("12");q.add("13");q.add("14");

// remove 10 elements from the head => tail LOOPS BACK in the elements[]
q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();q.poll();

// add 5 elements => the elements[] is not reallocated!
q.add("15");q.add("16");q.add("17");q.add("18");q.add("19");


q.poll();