管中窥豹:java队列(Queue)学习一--ArrayDeque

一、学习自

https://www.cnblogs.com/CarpenterLee/p/5468803.html
https://blog.csdn.net/u011240877/article/details/52860924
https://www.cnblogs.com/Dylansuns/archive/2017/04/30/6789161.html
https://www.cnblogs.com/lemon-flm/p/7877898.html
http://ifeve.com/java-blocking-queue/
ArrayBlockingQueue:http://www.cnblogs.com/skywang12345/p/3498652.html
LinkedBlockingQueue:http://www.cnblogs.com/skywang12345/p/3503458.html
LinkedBlockingDeque:http://www.cnblogs.com/skywang12345/p/3503480.html
ConcurrentLinkedQueue:http://www.cnblogs.com/skywang12345/p/3498995.html

二、概要

1.基本上,一个队列(Queue)就是一个先入先出(FIFO)的数据结构
2.队列有两种:单队列和循环队列。
3.ArrayDeque是双端队列,它既可以当作栈使用,也可以当作队列使用。底层是数组,而且是循环数组。
4.ArrayDeque是非线程安全的。
5.ArrayDeque中的head代表队列头指针,并不是数组第一个下标,tail代表尾指针并不是数组最后一个下标。看下图就能理解了。
6.根据下述示例走完了可以看到,remove(),pop(),poll()三个方法其实是一样的实现,都是用来删除队列头部数据
7.add()以及offer()将数据添加在队列尾部,结合删除操作实现“先进先出”效果。
8.push()将数据添加在队列头部,结合删除操作实现“后进先出”效果

三、学习示例加思考过程

本文源码基于jdk 1.8
      这里我以ArrayDeque为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ArrayDeque arrayDeque = new ArrayDeque<>();
for(int i = 0; i < 5; i++) {
arrayDeque.offer(i);
}
System.out.println(arrayDeque); // [0, 1, 2, 3, 4]
arrayDeque.remove();
System.out.println(arrayDeque); // [1, 2, 3, 4]
System.out.println(arrayDeque.peek()); // 1
System.out.println(arrayDeque); // [1, 2, 3, 4]
System.out.println(arrayDeque.poll()); // 1
System.out.println(arrayDeque); // [2, 3, 4]
System.out.println(arrayDeque.element()); // 2
System.out.println(arrayDeque); // [2, 3, 4]
arrayDeque.add("a");
System.out.println(arrayDeque); // [2, 3, 4, a]
arrayDeque.push("b");
System.out.println(arrayDeque); // [b, 2, 3, 4, a]
arrayDeque.pop();
System.out.println(arrayDeque); // [2, 3, 4, a]

      按照我们正常使用的方式来,先实例化然后根据调用的方法去一步步看源码实现,这样理解起来快些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// 这里我用的是默认无参构造函数,同时把内部定义的属性也给展示出来
transient Object[] elements; // 用来保存数据的数组
transient int head; // 队列的头部索引,初始化是0。我一开始想错了,以为这个是数组下标,但其实不是。
transient int tail; // 队列尾部索引,初始化也是0
private static final int MIN_INITIAL_CAPACITY = 8;
public ArrayDeque() {
elements = new Object[16]; // 初始化一个容量为16的数组
}
/*
* 接下来我调用了offer(),但是这里提前透露下,后面调用的add()竟然与offer()内部调用的方法一致
* 两个都列出来对比
* 注释里明确说了,offer()方法向队列尾部添加新元素
*/
public boolean offer(E e) {
return offerLast(e);
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
public boolean add(E e) {
addLast(e);
return true;
}
/*
* offer()内部调用的是offerLast(),而offerLast()内部与add()一模一样,只是换了个名字罢了
* 那么看看addLast()内部实现吧
*/
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e; // 这一步很容易理解,把刚刚添加的数据放在队列尾部
if ( (tail = (tail + 1) & (elements.length - 1)) == head) // 这一步涉及到“按位与”运算,一开始想错了
// 一开始以为是判断队列容量是否被占满的,
// 后来想想不大对,网上说其实就是判断
// 头指针与尾指针有没有相遇,相遇就扩容
// 看第一个链接
doubleCapacity();
}
private void doubleCapacity() {
assert head == tail; // assert 断言的意思,若为false则抛出异常
int p = head; // 缓存头索引
int n = elements.length; // 缓存数组长度
int r = n - p; // number of elements to the right of p
int newCapacity = n << 1; // 新长度 = n * 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; // 把头部索引重新赋值为0
tail = n; // 尾部索引赋值为旧数组的长度
}
/*
* offer()和add()我已经知道了,示例中接下来是remove,看看它怎么实现“先进先出”的
* 一共调用了3个方法
*/
public E remove() {
return removeFirst();
}
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,强行把队列头部置为null
head = (h + 1) & (elements.length - 1); // 头部索引自动往后移
return result; // 返回被删除的数据
}
/*
* 示例中接下来是peek()方法,继续看看它做了什么
* 注释说该方法返回队列的头部,代码也很简单,确实是返回头部
*/
public E peek() {
return peekFirst();
}
@SuppressWarnings("unchecked")
public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
}
/*
* 继续看看上述示例中用到的poll(),其实跟remove()是一样的,不解释了
*/
public E poll() {
return pollFirst();
}
/*
* 紧接着就是示例中用到的element(),代码也比较少,目的是返回队列头部的数据
*/
public E element() {
return getFirst();
}
public E getFirst() {
@SuppressWarnings("unchecked")
E result = (E) elements[head];
if (result == null)
throw new NoSuchElementException();
return result;
}
/*
* 顺着示例往下看,用到了push()和pop(),与add()和offer()以及remove(),poll()有何区别呢?
*/
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e; // 这里最终将传入的数据放到队列头部
if (head == tail)
doubleCapacity();
}
// pop()的内部代码与remove()也是一样的
public E pop() {
return removeFirst();
}

四、问题

1.源码内多次用到类似head = (head - 1) & (elements.length - 1)这种形式的代码,这是什么意思呢?
      这段代码用来解决下标越界的。这段代码相当于取余,同时解决了head为负值的情况。因为elements.length必需是2的指数倍,elements - 1就是二进制低位全1,跟head - 1相与之后就起到了取模的作用,如果head - 1为负数(其实只可能是-1),则相当于对其取相对于elements.length的补码。
      假设我实例化后立即调用push(),那么head - 1 == -1,elements.length - 1 == 15,出现了负数怎么办?&其实是二进制运算,那么负数怎么转为二进制呢?负数转换为二进制,就是将其绝对值的二进制的每一位变反(1变0,0变1)最后将变完了的数值加1,就完成了负数的补码运算。这样就变成了二进制。

1
2
3
4
5
十进制1的二进制:0001
十进制1的反码:1110
加1:1111
最后转为十进制:15
那么最后其实是 15 & 15 == 1111 & 1111 == 1111,最终还是15

      那么这时候,head变为了15,而tail仍然是0。从这里也可以看出,head已经tail并不对应数组第一和最后一个下标!!!

Newer Post

管中窥豹:线程基础以及Runnable、Thread源码学习

一、学习自https://www.zhihu.com/question/25532384https://www.zhihu.com/question/19901763http://ifeve.com/benefits/http://www.cnblogs.com/xrq730/category/73 …

继续阅读
Older Post

管中窥豹:TreeMap源码学习

一、学习自红黑树:http://www.cnblogs.com/skywang12345/p/3245399.htmlhttp://www.cnblogs.com/skywang12345/p/3310928.html 二、注意点TreeMap 是一个有序的key-value集合,它是通过红黑树实现 …

继续阅读