学而不思则罔,思而不学则殆
【Java】Java集合之ArrayDeque了解和原理分析
先看一下ArrayDeque的结构。
方法总结对比
1.队列入队方法
队列尾入队
方法 | 方法实现 | 是否抛出异常 |
---|---|---|
add | 内部调用addLast | NullPointerException |
offer | 内部调用offerLast | NullPointerException |
offerLast | 内部调用addLast | NullPointerException |
addLast | 实现入队逻辑 | NullPointerException |
队列头入队
方法 | 方法实现 | 是否抛出异常 |
---|---|---|
push | 内部调用addFirst | NullPointerException |
offerFirst | 内部调用addFirst | NullPointerException |
addFirst | 具体实现头部入队操作 | NullPointerException |
2.队列出队方法
队列头出队
方法 | 方法实现 | 是否抛出异常 |
---|---|---|
pop | 内部调用removeFirst | NoSuchElementException |
removeFirst | 内部调用pollFirst | NoSuchElementException |
poll | 内部调用pollFirst | 不抛出异常 |
pollFirst | 实现出队逻辑 | 不抛出异常 |
队列尾出队
方法 | 方法实现 | 是否抛出异常 |
---|---|---|
removeLast | 内部调用pollLast | NoSuchElementException |
pollLast | 实现移除队尾逻辑 | 不抛出异常 |
3.获取元素
获取队列头部元素
方法 | 方法实现 | 是否抛出异常 |
---|---|---|
peek | 内部调用peekFirst | 不抛出异常 |
peekFirst | 具体实现获取头部元素逻辑 | 不抛出异常 |
获取队列尾部元素
方法 | 方法实现 | 是否抛出异常 |
---|---|---|
peekLast | 具体实现获取队列尾部元素逻辑 | 不抛出异常 |
测试ArrayDeque
0.辅助方法打印队列信息
//ArrayDeque.java
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
{
/**
* 存储deque元素的数组。
* deque的容量就是这个数组的长度,总是2的幂。
* 数组永远不允许变成满的,除非是在addX方法中,当它变成满的时候会立即调整大小(参见doubleCapacity),从而避免头和尾相互绕来绕去以相等。
* 我们还保证所有不包含deque元素的数组单元格始终为空。
*/
transient Object[] elements; // non-private to simplify nested class access
transient int head;
transient int tail;
}
deque的容量就是这个数组的长度,总是2的幂。即16,32,64…
底层数据结构是一个数组+头尾下标,从而实现队列的数据结构。
通过反射打印底层数据结构的具体情况,便于我们掌握底层原理,辅助打印信息源码如下:
static void showDeque(String tag) throws NoSuchFieldException, IllegalAccessException {
Class<? extends Deque> aClassc = ArrayDeque.class;
System.out.println("\n----------------------------------------");
System.out.println(tag);
//打印队列上层情况
System.out.println("size:" + deque.size() + " deque:" + deque);
Field elements = aClassc.getDeclaredField("elements");
elements.setAccessible(true);
Object[] objects = (Object[]) elements.get(deque);
Field head = aClassc.getDeclaredField("head");
head.setAccessible(true);
Object h = head.get(deque);
Field tail = aClassc.getDeclaredField("tail");
tail.setAccessible(true);
Object t = tail.get(deque);
//通过方法,打印队列底层情况
System.out.println("elements.length:" + objects.length + " " + Arrays.toString(objects));
System.out.println("head:" + h + " tail:" + t);
System.out.println("----------------------------------------\n");
}
1.测试空集合
打印队列中为添加元素的时候的情况:
static Deque<String> deque = new ArrayDeque<>();
showDeque("空元素");
log展示如下:
----------------------------------------
空元素
size:0 deque:[]
elements.length:16 [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:0
----------------------------------------
默认数组元素长度为16,头尾下标为0;所以当头尾下标相等的时候,队列为空;
public boolean isEmpty() {
return head == tail;
}
/**
* Constructs an empty array deque with an initial capacity sufficient to hold 16 elements.
* 构造一个初始容量足以容纳16个元素的空数组deque。
*/
public ArrayDeque() {
elements = new Object[16];
}
2.测试入队和出队
showDeque("空元素");
deque.add("0");
showDeque("add 0 ");
deque.add("1");
showDeque("add 1");
deque.add("2");
showDeque("add 2");
deque.add("3");
showDeque("add 3");
deque.pop();
showDeque("pop");
----------------------------------------
add 0
size:1 deque:[0]
elements.length:16 [0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:1
----------------------------------------
----------------------------------------
add 1
size:2 deque:[0, 1]
elements.length:16 [0, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:2
----------------------------------------
----------------------------------------
add 2
size:3 deque:[0, 1, 2]
elements.length:16 [0, 1, 2, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:3
----------------------------------------
----------------------------------------
add 3
size:4 deque:[0, 1, 2, 3]
elements.length:16 [0, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:4
----------------------------------------
----------------------------------------
pop
size:3 deque:[1, 2, 3]
elements.length:16 [null, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:1 tail:4
----------------------------------------
入队的时候,往tail下标位置加入元素,tail自加1
出队的时候,head下标位置元素置为null,head自加1,
查看入队源码
/**
* Inserts the specified element at the end of this deque.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e the element to add
* @return {@code true} (as specified by {@link Collection#add})
* @throws NullPointerException if the specified element is null
*/
public boolean add(E e) {
addLast(e);
return true;
}
/**
* Inserts the specified element at the end of this deque.
*
* <p>This method is equivalent to {@link #add}.
*
* @param e the element to add
* @throws NullPointerException if the specified element is null
*/
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
add方法调用addLast方法。在addLast方法中可以看出队列元素不能为null,否则会抛出异常。先赋值队列尾部元素,然后计算新的队尾下标。在判断是否需要扩容,如果需要扩容就扩容。扩容在下一节中有细讲。
3.扩容逻辑
测试扩容
当队列快要满的时候,再入队就会发生数组扩机逻辑,一般发生扩容逻辑会导致运行效率降低一点。
扩容情况一
第一种很好理解,比如队列当前情况如下,此时要加入的元素在数组最后下标的最后一个位置,在入队15,就会发生扩容:
----------------------------------------
add 0..14
size:15 deque:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
elements.length:16 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, null]
head:0 tail:15
----------------------------------------
deque.add("15"); //此时入队,会触发底层数组扩容
showDeque("add 15");
扩容后的结果如下:
add 15
size:16 deque:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
elements.length:32 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:16
队列大小变成了16
数组长度变成了32,16 ==》32
扩容情况二
第二种情况,再入队一个元素即要发生扩容,但是新入队的元素不是数组下标的最后一个位置,比如:
----------------------------------------
add 21
size:15 deque:[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
elements.length:16 [16, 17, 18, 19, 20, 21, null, 7, 8, 9, 10, 11, 12, 13, 14, 15]
head:7 tail:6
----------------------------------------
此时队列长度为15,数组长度为16,在入队一个元素就会发生扩容。但是队列头下标是7,队列尾下标是6;
测试这种情况下的扩容:
deque.add("22");
showDeque("add 22");
结果:
----------------------------------------
add 22
size:16 deque:[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
elements.length:32 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:16
----------------------------------------
数组长度变为了32,说明发生了扩容。但是数组中的顺序发生了变化。那是怎么变化的呢?
扩容逻辑原理分析
public boolean add(E e) {
addLast(e);
return true;
}
/**
* Inserts the specified element at the end of this deque.
*
* <p>This method is equivalent to {@link #add}.
*
* @param e the element to add
* @throws NullPointerException if the specified element is null
*/
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head) //判断当前数组是否已经满了
doubleCapacity();//扩容逻辑
}
add方法调用了addLast。不能添加空元素,否则会报空指针异常。
判断队列是否需要扩容的主要判断是:
(tail = (tail + 1) & (elements.length - 1)) == head
根据前面两个扩容例子,我们测试分析一下:
扩容一分析
这是扩容前数据信息。
----------------------------------------
add 0..14
size:15 deque:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
elements.length:16 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, null]
head:0 tail:15
----------------------------------------
deque.add("15"); //此时入队,会触发底层数组扩容
按照逻辑,tail = 15 的下标添加"15"元素
tail = (tail+1)& (elements.length - 1) = 16 & 15 = 10000&1111 = 0 (保留低四位数据)
所以 tail == head .触发扩容
扩容二分析
这是扩容前数据信息。
----------------------------------------
add 21
size:15 deque:[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
elements.length:16 [16, 17, 18, 19, 20, 21, null, 7, 8, 9, 10, 11, 12, 13, 14, 15]
head:7 tail:6
----------------------------------------
deque.add("22"); //触发扩容
按照逻辑
tail = 6 的下标添加"22"元素
tail = (tail+1)& (elements.length - 1) = 7 & 15 = 0111&1111 = 7 (保留低四位数据)
所以 tail == head .触发扩容
所有上述的两种情况都会触发扩容。
扩容逻辑源码:
/**
* Doubles the capacity of this deque. Call only when full, i.e.,
* when head and tail have wrapped around to become equal.
*/
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; //新的数组长度 = oldLength * 2
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r); //第一次copy
System.arraycopy(elements, 0, a, r, p); //第二次copy
elements = a;
head = 0;
tail = n;
}
图示如下:
开始时候如图,a数组为空,原数组已经满了,现在需要移动到新的数组上来。(head = 7 tail = 7)
第一次复制,把橙色部分移动到新的数组前面(0…r-1)
第二次复制,把绿色部分移动到新的数组(r,oldLength-1)
这样便完成了一次扩容。
4.出队逻辑
队列目前情况
比如当前队列中存在四个元素:
----------------------------------------
add 3
size:4 deque:[0, 1, 2, 3]
elements.length:16 [0, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:4
----------------------------------------
deque.pop(); //出队
showDeque("pop");
出队源码
/**
* Pops an element from the stack represented by this deque. In other
* words, removes and returns the first element of this deque.
*
* <p>This method is equivalent to {@link #removeFirst()}.
*
* @return the element at the front of this deque (which is the top
* of the stack represented by this deque)
* @throws NoSuchElementException {@inheritDoc}
*/
public E pop() {
return removeFirst();
}
/**
* @throws NoSuchElementException {@inheritDoc}
*/
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];
// 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;
}
所以最终调用的pollFirst方法,前两个方法可能会抛出异常。
在pollFirst方法中:
- 先把队列头部元素取出,如果头部元素为null,直接退出
- 在把数组头部下标置为null
- 计算新的头部下标(主要是考虑队列头部下标在数组末尾的情况下怎么处理的)
- 返回队列头部元素
结果为:
----------------------------------------
pop
size:3 deque:[1, 2, 3]
elements.length:16 [null, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:1 tail:4
----------------------------------------
5.移除队尾元素
初始队列情况
----------------------------------------
add 3
size:4 deque:[0, 1, 2, 3]
elements.length:16 [0, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:4
----------------------------------------
移除队尾元素源码
/**
* @throws NoSuchElementException {@inheritDoc}
*/
public E removeLast() {
E x = pollLast();
if (x == null)
throw new NoSuchElementException();
return x;
}
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;
}
removeLast方法调用pollLast方法,pollLast实现移除逻辑,这两个方法都会抛出
6.队头插入元素
当前队列情况
----------------------------------------
add 3
size:4 deque:[0, 1, 2, 3]
elements.length:16 [0, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:4
----------------------------------------
deque.offerFirst("-1"); //队列头部插入“-1”
showDeque("offerFirst(\"-1\")");
查看插入源码
/**
* Inserts the specified element at the front of this deque.
*
* @param e the element to add
* @return {@code true} (as specified by {@link Deque#offerFirst})
* @throws NullPointerException if the specified element is null
*/
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
/**
* Inserts the specified element at the front of this deque.
*
* @param e the element to add
* @throws NullPointerException if the specified element is null
*/
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
addFirst插入元素后,判断数组是否需要扩容,如果需要就扩容。扩容逻辑前面已经讲过了。
结束
ArrayDeque的方法没有分析完,但是其他方法分析大同小异,只要掌握了分析方法+调试方法,很容了解其底层原理。欢迎大家一起学习进步。