Javadeque_deque接口

Javadeque_deque接口Queue也是Java集合框架中定义的一种接口,直接继承自Collection接口。除了基本的Collection接口规定测操作外,Queue接口还定义一组针对队列的特殊操作。通常来说,Queue是按照先进先出(FIFO)的方式来管理其中的元素的,但是优先队列是一个例外。Deque接口继承自Queue接口,但Deque支持同时从两端添加或移除元素,因此又被成为双端队列。鉴…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

Queue 也是 Java 集合框架中定义的一种接口,直接继承自 Collection 接口。除了基本的 Collection 接口规定测操作外,Queue 接口还定义一组针对队列的特殊操作。通常来说,Queue 是按照先进先出(FIFO)的方式来管理其中的元素的,但是优先队列是一个例外。

Deque 接口继承自 Queue接口,但 Deque 支持同时从两端添加或移除元素,因此又被成为双端队列。鉴于此,Deque 接口的实现可以被当作 FIFO队列使用,也可以当作LIFO队列(栈)来使用。官方也是推荐使用 Deque 的实现来替代 Stack。

ArrayDeque 是 Deque 接口的一种具体实现,是依赖于可变数组来实现的。ArrayDeque 没有容量限制,可根据需求自动进行扩容。ArrayDeque不支持值为 null 的元素。

下面基于JDK 8中的实现对 ArrayDeque 加以分析。

方法概览

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

public interface Queue extends Collection {

//向队列中插入一个元素,并返回true

//如果队列已满,抛出IllegalStateException异常

boolean add(E e);

//向队列中插入一个元素,并返回true

//如果队列已满,返回false

boolean offer(E e);

//取出队列头部的元素,并从队列中移除

//队列为空,抛出NoSuchElementException异常

E remove();

//取出队列头部的元素,并从队列中移除

//队列为空,返回null

E poll();

//取出队列头部的元素,但并不移除

//如果队列为空,抛出NoSuchElementException异常

E element();

//取出队列头部的元素,但并不移除

//队列为空,返回null

E peek();

}

Deque 提供了双端的插入与移除操作,如下表:

First Element (Head)Last Element (Tail)

Throws exception

Special value

Throws exception

Special value

Insert

addFirst(e)

offerFirst(e)

addLast(e)

offerLast(e)

Remove

removeFirst()

pollFirst()

removeLast()

pollLast()

Examine

getFirst()

peekFirst()

getLast()

peekLast()

Deque 和 Queue 方法的的对应关系如下:

Queue MethodEquivalent Deque Method

add(e)

addLast(e)

offer(e)

offerLast(e)

remove()

removeFirst()

poll()

pollFirst()

element()

getFirst()

peek()

peekFirst()

Deque 和 Stack 方法的对应关系如下:

Stack MethodEquivalent Deque Method

push(e)

addFirst(e)

pop()

removeFirst()

peek()

peekFirst()

ArrayList 实现了 Deque 接口中的所有方法。因为 ArrayList 会根据需求自动扩充容量,因而在插入元素的时候不会抛出IllegalStateException异常。

底层结构

1

2

3

4

5

6

7

8

//用数组存储元素

transient Object[] elements; // non-private to simplify nested class access

//头部元素的索引

transient int head;

//尾部下一个将要被加入的元素的索引

transient int tail;

//最小容量,必须为2的幂次方

private static final int MIN_INITIAL_CAPACITY = 8;

在 ArrayDeque 底部是使用数组存储元素,同时还使用了两个索引来表征当前数组的状态,分别是 head 和 tail。head 是头部元素的索引,但注意 tail 不是尾部元素的索引,而是尾部元素的下一位,即下一个将要被加入的元素的索引。

初始化

ArrayDeque 提供了三个构造方法,分别是默认容量,指定容量及依据给定的集合中的元素进行创建。默认容量为16。

1

2

3

4

5

6

7

8

9

10

11

12

public ArrayDeque() {

elements = new Object[16];

}

public ArrayDeque(int numElements) {

allocateElements(numElements);

}

public ArrayDeque(Collection extends E> c) {

allocateElements(c.size());

addAll(c);

}

ArrayDeque 对数组的大小(即队列的容量)有特殊的要求,必须是 2^n。通过 allocateElements方法计算初始容量:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

private void allocateElements(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

}

elements = new Object[initialCapacity];

}

>>>是无符号右移操作,|是位或操作,经过五次右移和位或操作可以保证得到大小为2^k-1的数。看一下这个例子:

1

2

3

4

0 0 0 0 1 ? ? ? ? ? //n

0 0 0 0 1 1 ? ? ? ? //n |= n >>> 1;

0 0 0 0 1 1 1 1 ? ? //n |= n >>> 2;

0 0 0 0 1 1 1 1 1 1 //n |= n >>> 4;

在进行5次位移操作和位或操作后就可以得到2^k-1,最后加1即可。这个实现还是很巧妙的。

添加元素

向末尾添加元素:

1

2

3

4

5

6

7

8

9

10

11

public void addLast(E e) {

if (e == null)

throw new NullPointerException();

//tail 中保存的是即将加入末尾的元素的索引

elements[tail] = e;

//tail 向后移动一位

//把数组当作环形的,越界后到0索引

if ( (tail = (tail + 1) & (elements.length – 1)) == head)

//tail 和 head相遇,空间用尽,需要扩容

doubleCapacity();

}

这段代码中,(tail = (tail + 1) & (elements.length – 1)) == head这句有点难以理解。其实,在 ArrayDeque 中数组是当作环形来使用的,索引0看作是紧挨着索引(length-1)之后的。参考下面的图片:

926a8252955e89bd92ef548f5fdd1095.png

那么为什么(tail + 1) & (elements.length – 1)就能保证按照环形取得正确的下一个索引值呢?这就和前面说到的 ArrayDeque 对容量的特殊要求有关了。下面对其正确性加以验证:

1

2

3

4

5

length = 2^n,二进制表示为: 第 n 位为1,低位 (n-1位) 全为0

length – 1 = 2^n-1,二进制表示为:低位(n-1位)全为1

如果 tail + 1 <= length – 1,则位与后低 (n-1) 位保持不变,高位全为0

如果 tail + 1 = length,则位与后低 n 全为0,高位也全为0,结果为 0

可见,在容量保证为 2^n 的情况下,仅仅通过位与操作就可以完成环形索引的计算,而不需要进行边界的判断,在实现上更为高效。

向头部添加元素的代码如下:

1

2

3

4

5

6

7

public void addFirst(E e) {

if (e == null) //不支持值为null的元素

throw new NullPointerException();

elements[head = (head – 1) & (elements.length – 1)] = e;

if (head == tail)

doubleCapacity();

}

其它的诸如add,offer,offerFirst,offerLast等方法都是基于上面这两个方法实现的,不再赘述。

扩容

在每次添加元素后,如果头索引和尾部索引相遇,则说明数组空间已满,需要进行扩容操作。 ArrayDeque 每次扩容都会在原有的容量上翻倍,这也是对容量必须是2的幂次方的保证。

18011cf171d345ed3a04637f23bbf22f.png

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

private void doubleCapacity() {

assert head == tail; //扩容时头部索引和尾部索引肯定相等

int p = head;

int n = elements.length;

//头部索引到数组末端(length-1处)共有多少元素

int r = n – p; // number of elements to the right of p

//容量翻倍

int newCapacity = n << 1;

//容量过大,溢出了

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;

}

移除元素

ArrayDeque支持从头尾两端移除元素,remove方法是通过poll来实现的。因为是基于数组的,在了解了环的原理后这段代码就比较容易理解了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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;

}

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;

}

获取队头和队尾的元素

1

2

3

4

5

6

7

8

9

10

@SuppressWarnings(“unchecked”)

public E peekFirst() {

// elements[head] is null if deque empty

return (E) elements[head];

}

@SuppressWarnings(“unchecked”)

public E peekLast() {

return (E) elements[(tail – 1) & (elements.length – 1)];

}

迭代器

ArrayDeque 在迭代是检查并发修改并没有使用类似于 ArrayList 等容器中使用的 modCount,而是通过尾部索引的来确定的。具体参考 next 方法中的注释。但是这样不一定能保证检测到所有的并发修改情况,加入先移除了尾部元素,又添加了一个尾部元素,这种情况下迭代器是没法检测出来的。

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

private class DeqIterator implements Iterator {

/**

* Index of element to be returned by subsequent call to next.

*/

private int cursor = head;

/**

* Tail recorded at construction (also in remove), to stop

* iterator and also to check for comodification.

*/

private int fence = tail;

/**

* Index of element returned by most recent call to next.

* Reset to -1 if element is deleted by a call to remove.

*/

private int lastRet = -1;

public boolean hasNext() {

return cursor != fence;

}

public E next() {

if (cursor == fence)

throw new NoSuchElementException();

@SuppressWarnings(“unchecked”)

E result = (E) elements[cursor];

// This check doesn’t catch all possible comodifications,

// but does catch the ones that corrupt traversal

// 如果移除了尾部元素,会导致tail != fence

// 如果移除了头部元素,会导致 result == null

if (tail != fence || result == null)

throw new ConcurrentModificationException();

lastRet = cursor;

cursor = (cursor + 1) & (elements.length – 1);

return result;

}

public void remove() {

if (lastRet < 0)

throw new IllegalStateException();

if (delete(lastRet)) { // if left-shifted, undo increment in next()

cursor = (cursor – 1) & (elements.length – 1);

fence = tail;

}

lastRet = -1;

}

public void forEachRemaining(Consumer super E> action) {

Objects.requireNonNull(action);

Object[] a = elements;

int m = a.length – 1, f = fence, i = cursor;

cursor = f;

while (i != f) {

@SuppressWarnings(“unchecked”) E e = (E)a[i];

i = (i + 1) & m;

if (e == null)

throw new ConcurrentModificationException();

action.accept(e);

}

}

}

除了 DeqIterator,还有一个反向的迭代器 DescendingIterator,顺序和 DeqIterator 相反。

小结

ArrayDeque 是 Deque 接口的一种具体实现,是依赖于可变数组来实现的。ArrayDeque 没有容量限制,可根据需求自动进行扩容。ArrayDeque 可以作为栈来使用,效率要高于 Stack;ArrayDeque 也可以作为队列来使用,效率相较于基于双向链表的 LinkedList 也要更好一些。注意,ArrayDeque 不支持为 null 的元素。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/191413.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • proxmox集群节点崩溃处理

    proxmox集群节点崩溃处理

  • 推荐 21 款博主常用 Windows 软件「建议收藏」

    经常会有朋友让我推荐一些好用的软件,因此我打算写一篇博客来介绍一下这些我认为爱不释手的软件

  • 字符串常量池 运行时常量池_常量池中的字符串是对象吗

    字符串常量池 运行时常量池_常量池中的字符串是对象吗详细介绍了字符串常量池以及其产生的相关问题,并对String类相关操作和String类中的intern()方法进行了详细解析。

  • Django(61)认证组件源码分析

    Django(61)认证组件源码分析认证组件源码入口APIView下的dispatch下的self.initial(request,*args,**kwargs),源码如下:definitial(self,request,

  • MariaDB安装Win10

    MariaDB安装Win10本次搭建mysql数据,选择了是和mysql类似的MariaDB,完全可以满足日常的使用需求,且命令和mysql没有太大的区别。对应MariaDB下载地址:https://downloads.mariadb.org/解压下载完成的文件,这里我解压到了C盘,路径:C:\mariadb-10.5.3-winx64使用win+R,输入CMD,进入DOS控制台。输入命令cdC:\mariadb-10.5.3-winx64,进入MariaDB的对应的路径中执行安装的命令mysqld.exe–..

  • 海量数据处理技巧

    海量数据处理技巧数据时代来临,数据量的爆炸式增长是最为显著的特征。当高性能硬件的普及还跟不上这样的数据大潮时,如何在有限的时空资源内处理海量数据成为了计算机科学以及数理统计等领域最大的挑战。所谓“数据处理”,在本文中特指通过计算机技术,对海量数据进行存储、统计、查询等操作。我将在下面介绍一些基本的海量数据处理的方法,供大家参考。需要明确的一点是,现实情况复杂多变,所以对于海量数据处理这样大的主题,是不可能用一…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号