前两篇讲过,ArrayList使用数组性能很高,LinkedList更擅长处理头部数据。而ArrayDeque即使用数组,又擅长处理头部数据。相关特点前面章节已经详细介绍过,所以,要理解ArrayDeque,请先理解前几篇文章的内容。
双端队列
上一篇讲过,LinkedList用链表实现了Deque,即双端队列。ArrayDeque则用数组实现了Deque,Deque用法参考第5篇结尾。现在eclipse中调试以下代码
ArrayDeque deque = new ArrayDeque();
System.out.println("断点1");
deque.add(1);
System.out.println("断点2");
for(int i=2;i<14;i++){
deque.add(i);
}
System.out.println("断点3");
deque.addFirst(0);
System.out.println("断点4");
deque.addFirst(-1);
System.out.println("断点5");
deque.addFirst(-2);
System.out.println("断点6");
- 断点1:和ArrayList一样,它用一个数组参数element来存储数据,默认初始容量是16。也和LinkedList一样,用两个参数来记录头部和尾部,即head和tail,初始都是0,此时没有添加数据。
- 断点2:添加第一个数据,和ArrayList一样默认在尾部添加。此时头部head仍然是0,而尾部tail变成了1,即下一次的添加位置。
- 断点3:此时共添加了13条数据,tail指向坐标13,head仍然是0。
- 断点4:重点来了,现在不再使用add()从尾部添加,而是用addFirst(0)从头部添加。注意:head头部变成了15,新添加的数据存储到了这里。此时队列头部第一个数据是位置15,第二个数据是位置0,第三个是位置1,有点像循环队列。这样就无需像ArrayList那样移动之前的数据了,从而实现了数组高效的从头部添加数据。
- 断点5:继续从头部添加,添加到了倒数第二的位置,同上面的原理一样,之前的数据完全不需要移动。
- 断点6:仍然从头部添加,显然会添加到倒数第三的位置,此时整个数组被填满。容量从16扩容为32,即按2倍容量扩容。注意:扩容时会进行数据整理,最后从头部添加的三个数据本来是在数组结尾,现在移动到了头部,成了真正的头部,如下图,head和tail也相应变成0和16。
先删后增
上面解释了,从头部添加,实际上会添加到数组结尾。但并不是任何时候都是这样。调试以下代码 ArrayDeque deque = new ArrayDeque();
deque.add(1);
deque.add(2);
deque.add(3);
deque.add(4);
deque.add(5);
deque.poll();
deque.poll();
System.out.println("断点1");
deque.addFirst(0);
System.out.println("断点2");
- 断点1:先添加5条数据,然后使用poll方法从头部取出两条数据,也就是删除。删除其实就是赋null值。
- 断点2:此时再执行addFirst()从头部添加,不会再添加到结尾,而是添加到了之前头部的前面。因此,无论怎么添加和删除,队列数据始终都是连续的。
数组容量
前面说过,ArrayDeque的数组默认长度是16。也可以像ArrayList那样,在初始时设置一个参数,直接分配较大的容量,以解决反复扩容的性能开销。我现在随便设置一个数字,如19。如下
ArrayDeque deque = new ArrayDeque(19);
通过调试发现,此时数组elements的长度并不是19,而是32。因为其长度有一定限制标准,最小是8,只能按8的2倍长度扩容,所以其长度只能是8、16、32、64、128等。当设置参数为19时,它的长度会是其中一个刚刚大于19的数字,即32。因为这样的数字适合进行一些二进制运算,ArrayDeque正是通过相关的二进制算法来进行定位,以及确定何时扩容、何时循环链表等。这里不多讨论,知道即可。
ArrayDeque特性和场景
- 和LinkedList一样,记录了头和尾的位置,擅长处理头部和尾部的数据,适用于栈和队列。但是并没有提供查询和处理其他位置数据的方法,不像ArrayList可以适应通用场景。
- 由于使用数组,而且不需要移动位置,性能肯定高于LinkedList。上一篇已经解释过原因。另外,jdk官方文档上也明确指出,当用于队列时,ArrayDeque比LinkedList更快。
- 当删除大量数据时,数组容量不会缩小,删除的位置仍然会赋null值。如果之后又没有足够的数据添加进来,大量的null会让费大量内存。而LinkedList每个结点被删除后,都会单独被内存回收,不会一直占用内存。
- 数组初始长度为16,最小只能指定为8,原因上面已经说了。假如数组中只有一个元素,仍然会占用8个内存位置。如果要创建大量这样的数组,同样非常让费内存。ArrayList可以指定任意长度,不存在这样的问题。LinkedList每个结点是独立创建的,不会占用多余空间,也不存在这样的问题。