Queue接口
1 public abstract boolean add(E paramE); 2 public abstract boolean offer(E paramE); // 加入元素 3 public abstract E remove(); 4 public abstract E poll(); // 获取元素
Queue接口提供了以上几个方法。
Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。
它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。
如果要使用前端而不移出该元素,使用element()或者peek()方法。
ArrayDeque
ArrayDeque的特点
- 大小自增长的队列
- 内部使用数组存储数据
- 线程不安全
- 内部数组长度为8、16、32….. 2的n次方
- 头指针head从内部数组的末尾开始,尾指针tail从0开始,在头部插入数据时,head减一,在尾部插入数据时,tail加一。当head==tail时说明数组的容量满足不了当前的情况,此时需要扩大容量为原来的二倍。
核心思想图
代码解析
- 分配数组大小
1 private void allocateElements(int numElements) { 2 int initialCapacity = MIN_INITIAL_CAPACITY; 3 // 如果指定的数组长度大于最小的值,需要计算数组大小。 4 // 算法利用或运算和右移运算,计算结果始终为2的n次方,比如numElements的值为4、5、6、7时,initialCapacity的结果为8;numElements的值为8、9、10、11、12、13、14、15时,initialCapacity的结果为16。 5 // 设计数组的长度为2的n次方是为循环链表考虑的,后面会解释。 6 if (numElements >= initialCapacity) { 7 initialCapacity = numElements; 8 initialCapacity |= (initialCapacity >>> 1); 9 initialCapacity |= (initialCapacity >>> 2); 10 initialCapacity |= (initialCapacity >>> 4); 11 initialCapacity |= (initialCapacity >>> 8); 12 initialCapacity |= (initialCapacity >>> 16); 13 initialCapacity++; 14 // 如果值太大溢出(即值小于0),需要缩小2倍 15 if (initialCapacity < 0) // Too many elements, must back off 16 initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements 17 } 18 elements = new Object[initialCapacity]; 19 }
- 扩大数组长度
1 private void doubleCapacity() { 2 // assert head == tail; 3 int p = head; 4 int n = elements.length; 5 // 头指针右边的长度 6 int r = n - p; 7 // 扩大2倍 8 int newCapacity = n << 1; 9 // 检测新的数组长度是否太大 10 if (newCapacity < 0) 11 throw new IllegalStateException("Sorry, deque too big"); 12 Object[] a = new Object[newCapacity]; 13 // 复制头指针右边的数据 14 System.arraycopy(elements, p, a, 0, r); 15 // 复制头指针左边的数据 16 System.arraycopy(elements, 0, a, r, p); 17 elements = a; 18 // 初始化头指针为0 19 head = 0; 20 // 初始化尾指针为n 21 tail = n; 22 }
- 在头部插入数据
1 public void addFirst(E e) { 2 if (e == null) 3 throw new NullPointerException("e == null"); 4 // 在头部插入数据,头指针向左移动1,即减一,elements.length为2的n次方,所以elements.lenght-1的二进制表示为1111....1111。 5 // 特殊情况:假设数组的长度为16,初始化时head为0,那么(head - 1) & (elements.length - 1)就是-1 & 15的值,负数的二进制为对应正数的二进制的补码,-1即为11111....1111,那么-1 & 15 = 15,如上图,当首次向队列头部插入数据时,head在数组的末尾。 6 elements[head = (head - 1) & (elements.length - 1)] = e; 7 // 头指针和尾指针相等时(即相遇),表示当前的数组长度不够,需要扩大数组长度 8 if (head == tail) 9 doubleCapacity(); 10 }
- 在尾部插入数据
1 public void addLast(E e) { 2 if (e == null) 3 throw new NullPointerException("e == null"); 4 elements[tail] = e; 5 // tail向右移动,如果头指针和尾指针相等时(即相遇),表示当前的数组长度不够,需要扩大数组长度 6 if ( (tail = (tail + 1) & (elements.length - 1)) == head) 7 doubleCapacity(); 8 }
- 遍历
1 private class DeqIterator implements Iterator<E> { 2 // 从头指针位置开始遍历 3 private int cursor = head; 4 private int fence = tail; 5 private int lastRet = -1; 6 7 public boolean hasNext() { 8 return cursor != fence; 9 } 10 11 public E next() { 12 if (cursor == fence) 13 throw new NoSuchElementException(); 14 @SuppressWarnings("unchecked") E result = (E) elements[cursor]; 15 16 if (tail != fence || result == null) 17 throw new ConcurrentModificationException(); 18 lastRet = cursor; 19 // 每次加一,利用与运算,形成循环。比如,数组的长度为16,当cursor递增到15时,下面的计算应为16 & 15 = 0,cursor的值变为了0. 20 cursor = (cursor + 1) & (elements.length - 1); 21 return result; 22 } 23 24 public void remove() { 25 if (lastRet < 0) 26 throw new IllegalStateException(); 27 if (delete(lastRet)) { // if left-shifted, undo increment in next() 28 cursor = (cursor - 1) & (elements.length - 1); 29 fence = tail; 30 } 31 lastRet = -1; 32 } 33 }
有以下几点总结:
1)ArrayDeque有两个类属性,head和tail,两个指针。
2)ArrayDeque通过一个数组作为载体,其中的数组元素在add等方法执行时不移动,发生变化的只是head和tail指针,而且指针是循环变化,数组容量不限制。
3)offer方法和add方法都是通过其中的addLast方法实现,每添加一个元素,就把元素加到数组的尾部,此时,head指针没有变化,而tail指针加一,因为指针是循环加的,
所以当tail追上head((this.tail = this.tail + 1 & this.elements.length - 1) == this.head)时,数组容量翻一倍,继续执行。
4)remove方法和poll方法都是通过其中的pollFirst方法实现,每移除一个元素,该元素所在位置变成null,此时,tail指针没有变化,而head指针加一,当数组中没有数据时,返回null。
5)因为ArrayDeque不是线程安全的,所以,用作堆栈时快于 Stack,在用作队列时快于 LinkedList。