最近盯上了java collection
框架中一个类——ArrayDeque
。不少人可能没用过甚至没据说过这个类(i'm sorry,what's fu*k this?),毕竟你坐在面试官面前的时候,关于数组链表的掌握状况,99%的可能性听到问题会是:说说ArrayList和LinkedList的区别?
今天从ArrayDeque入手,换一个角度来检验下咱们是否真正掌握了数组、链表。java
不着急分析这个类的核心方法,先看下它的父类和接口,以便在Java Collection宇宙中找准它的定位,顺带从宏观角度窥探下Java Collection框架设计。面试
父类是AbstractCollection
,看下它的方法segmentfault
add、addAll、remove、clear、iterator、size……是否是都很常见?你经常使用的xxList中,常常会看到这些方法吧?能够说,AbstractCollection这个抽象类,是这种结构(数组、链表等等)的骨架!数组
首先是Queue接口,定义出了最基本的队列功能:框架
那么Deque接口呢?
入眼各类xxFirst、xxLast,这种定义决定了它是双端队列的表明!this
相继看了父类和接口,楼主你到底想表达啥?嘿嘿,别急,我再反问一个经典问题——抽象类和接口有什么区别?
你可能会有各类回答,好比抽象类能本身有本身的实现之类的。不能说不对,但这种答案至关于只停留在奇技淫巧层面,未得正统。以设计角度来看,实际上是is-a(抽象类)和has-a(接口)的区别!spa
好比定义汽车AbstractCar,会规定有轮子有发动机能跑的就是汽车;各家厂商生产的汽车都逃不出这个范畴,甭管你是大众宝马玛莎拉蒂。设计
有些汽车多了座椅加热;有些增设了天窗打开功能。但这些功能都是加强型的,并非每种汽车都会有!指针
抽象类和接口合理的组合,就产生了奇妙的效果:既能保证种族(类)的结构,又能对其进行扩展(接口)。给出你们熟悉的ArrayList和LinkedList,仔细感觉下:code
这种设计不只仅限于Java Collection,开源框架中也是如此,好比Spring IOC中的Context、Factory那部分……
回归到本文的主角 ArrayDeque,既然它实现了Deque,天然具有双端队列的特性。类名中的 Array姓氏,无时无刻不在提醒咱们,它是基于数组实现的。
类注释中,有句话引发了个人注意:
/** * This class is likely to be faster than * {@link Stack} when used as a stack, and faster than {@link LinkedList} * when used as a queue. */
(Stack先无论)注释中后半句说,ArrayDeque做为队列时比LinkedList快,看看它是怎么办到的!
三大属性:
transient Object[] elements; //基于数组实现 transient int head; //头指针 transient int tail; //尾巴指针
技术敏感的同窗已经能猜到它是怎么实现的了:数组做为基底,两个指分指头尾,插入删除操做时移动指针;若是头尾指针重合,则须要扩容……
下面看看源码实现,是否和咱们猜想的一致。
private static final int MIN_INITIAL_CAPACITY = 8; // ****** Array allocation and resizing utilities ****** private static int calculateSize(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 } return initialCapacity; }
规定最小值MIN_INITIAL_CAPACITY = 8
,若是入参小于8,数组大小就定义成8;若是大于等于8,这一通右移是啥操做?假如咱们传入了16,二进制10000,逐步分析下:
1.initialCapacity |= (initialCapacity >>> 1)
右移1位做|
操做,10000->01000,'或' 操做后11000
2.initialCapacity |= (initialCapacity >>> 2)
接上一步,右移2位做|
操做,11000->00110,'或' 操做后11110
3.initialCapacity |= (initialCapacity >>> 4)
接上一步,右移4位做|
操做,11110->00001,'或' 操做后 11111
……
后面就两步都是11111 | 00000,结果就是 11111
4.initialCapacity++
二进制数11111,+1以后100000,转换成十进制32
最终的负值判断(用于处理超int正向范围状况),先不考虑。
结论:这些'或' 操做,最终获得了大于入参的2的次幂中最小的一个。
底层数组始终是2的次幂,为何如此?带着这个问题继续往下分析
// The main insertion and extraction methods are addFirst, // addLast, pollFirst, pollLast. The other methods are defined in // terms of these.
以上注释有云,核心方法就4个,咱们从add方法入手。
public void addFirst(E e) { if (e == null) throw new NullPointerException(); elements[head = (head - 1) & (elements.length - 1)] = e; //关键 if (head == tail) doubleCapacity(); }
head = (head - 1) & (elements.length - 1)
,玄机就在这里。若是你对1.8的HashMap
足够了解,就会知道hashmap的数组大小一样始终是2的次幂。其中很重要的一个缘由就是:当lengh是2的次幂的时候,某数字 x 的操做 x & (length - 1)
等价于 x % length
,而对二进制的计算机来讲 & 操做要比 % 操做效率更好!
并且head = (head - 1) & (elements.length - 1)
,(head初始值0)第一次就将head指针定位到数组末尾了。
画图分析下:
可见,head指针从后向前移动。
public void addLast(E e) { if (e == null) throw new NullPointerException(); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); }
addLast和addFirst原理相同,只是addLast控制tail指针,从前向后移动!
上图中再作一次add操做,指针将会重合。好比,再一次addFirst以后:
if (head == tail) doubleCapacity(); //扩容触发
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; //左移,等价乘2,依然保持2的次幂 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; }
经过数组拷贝和从新调整指针,完成了扩容。
至于pollFirst、pollLast是addFirst、addLast的相反操做,原理类似,很少作分析。
回到那个问题:做为队列时,ArrayDeque效率为何会比LinkedList更好?
我以为由于LinkedList做为队列实现,新增修改要多修改节点的pre、next指针,且地址非连续,寻址时也会比array花更多时间。