简介
Queue是一种很常见的数据结构类型,在java里面Queue是一个接口,它只是定义了一个基本的Queue应该有哪些功能规约。实际上有多个Queue的实现,有的是采用线性表实现,有的基于链表实现。还有的适用于多线程的环境。java中具有Queue功能的类主要有如下几个:AbstractQueue, ArrayBlockingQueue, ConcurrentLinkedQueue, LinkedBlockingQueue, DelayQueue, LinkedList, PriorityBlockingQueue, PriorityQueue和ArrayDqueue。在本文中,我们主要讨论常用的两种实现:LinkedList和ArrayDeque。
Queue
Queue本身是一种先入先出的模型(FIFO),和我们日常生活中的排队模型很类似。根据不同的实现,他们主要有数组和链表两种实现形式。如下图:
因为在队列里和我们日常的模型很近似,每次如果要出队的话,都是从队头移除。而如果每次要加入新的元素,则要在队尾加。所以我们要在队列里保存队头和队尾。
在jdk里几个常用队列实现之间的类关系图如下:
我们可以看到,Deque也是一个接口,它继承了Queue的接口规范。我们要讨论的LinkedList和ArrayDeque都是实现Deque接口,所以,可以说他们俩都是双向队列。具体的实现我们会在后面讨论。Queue作为一个接口,它声明的几个基本操作无非就是入队和出队的操作,具体定义如下:
public interface Queue<E> extends Collection<E> {
boolean add(E e); // 添加元素到队列中,相当于进入队尾排队。
boolean offer(E e); //添加元素到队列中,相当于进入队尾排队.
E remove(); //移除队头元素
E poll(); //移除队头元素
E element(); //获取但不移除队列头的元素
E peek(); //获取但不移除队列头的元素
}
有了这些接口定义的规约,我们就可以很容易的在后续的详细实现里察看具体细节。
Deque
按照我们一般的理解,Deque是一个双向队列,这将意味着它不过是对Queue接口的增强。如果仔细分析Deque接口代码的话,我们会发现它里面主要包含有4个部分的功能定义。1. 双向队列特定方法定义。 2. Queue方法定义。 3. Stack方法定义。 4. Collection方法定义。
第3,4部分的方法相当于告诉我们,具体实现Deque的类我们也可以把他们当成Stack和普通的Collection来使用。这也是接口定义规约带来的好处。这里我们就不再赘述。
我们重点来对Queue相关的定义方法做一下概括:
add相关的方法有如下几个:
boolean add(E e);
boolean offer(E e);
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
这里定义了add, offer两个方法,从doc说明上来看,两者的基本上没什么区别。之所以定义了这两个方法是因为Deque继承了Collection, Queue两个接口,而这两个接口中都定义了增加元素的方法声明。他们本身的目的是一样的,只是在队列里头,添加元素肯定只是限于在队列的头或者尾添加。而offer作为一个更加适用于队列场景中的方法,也有存在的意义。他们的实现基本上一样,只是名字不同罢了。
remove相关的方法:
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E remove();
E poll();
这里remove相关的方法poll和remove也很类似,他们存在的原因也和前面一样。
get元素相关的方法:
E getFirst();
E getLast();
E peekFirst();
E peekLast();
E element();
E peek();
peek和element方法和前面提到的差别有点不一样,element方法是在队列为空的时候抛异常,而element则是返回null。
ok,有了前面这些对方法操作的分门别类,我们后面分析起具体实现就更方便了。
ArrayDeque
有了我们前面几篇分析的基础,我们可以很容易猜到ArrayDeque的内部实现机制。它的内部使用一个数组来保存具体的元素,然后分别使用head, tail来指示队列的头和尾。他们的定义如下:
private transient E[] elements;
private transient int head;
private transient int tail;
private static final int MIN_INITIAL_CAPACITY = 8;
ArrayDeque的默认长度为8,这么定义成2的指数值也是有一定好处的。在后面调整数组长度的时候我们会看到。关于tail需要注意的一点是tail所在的索引位置是null值,在它前面的元素才是队列中排在最后的元素。
调整元素长度
在调整元素长度
部分,ArrayDeque采用了两个方法来分配。一个是allocateElements,还有一个是doubleCapacity。allocateElements方法用于构造函数中根据指定的参数设置初始数组的大小。而doubleCapacity则用于当数组元素不够用了扩展数组长度。
下面是allocateElements方法的实现:
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 = (E[]) new Object[initialCapacity];
}
这部分代码里最让人困惑的地方就是对initialCapacity做的这一大堆移位和或运算。首先通过无符号右移1位,与原来的数字做或运算,然后在右移2、4、8、16位。这么做的目的是使得最后生成的数字尽可能每一位都是1。而且很显然,如果这个数字是每一位都为1,后面再对这个数字加1的话,则生成的数字肯定为2的若干次方。而且这个数字也肯定是大于我们的numElements值的最小2的指数值。这么说来有点绕。我们前面折腾了大半天,就为了求一个2的若干次方的数字,使得它要大于我们指定的数字,而且是最接近这个数字的数。这样子到底是为什么呢?因为我们后面要扩展数组长度的话,有了它这个基础我们就可以判断这个数字是不是到了2的多少多少次方,它增长下去最大的极限也不过是2的31次方。这样他每次的增长刚好可以把数组可以允许的长度给覆盖了,不会出现空间的浪费。比如说,我正好有一个数组,它的长度比Integer.MAX_VALUE的一半要大几个元素,如果我们这个时候设置的值不是让它为2的整数次方,那么直接对它空间翻倍就导致空间不够了,但是我们完全可以设置足够空间来容纳的。
我们现在再来看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;
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 = (E[])a;
head = 0;
tail = n;
}
有了前面的讨论,它只要扩展空间容量的时候左移一位,这就相当于空间翻倍了。如果长度超出了允许的范围,就会发生溢出,返回的结果就会成为一个负数。这就是为什么有 if (newCapacity < 0)这一句来抛异常。
添加元素
我们先看看两个主要添加元素的方法add和offer:
public boolean add(E e) {
addLast(e);
return true;
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
public boolean offer(E e) {
return offerLast(e);
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
很显然,他们两个方法的底层实现实际上是一样的。这里要注意的一个地方就是我们由于不断的入队和出队,可能head和tail都会移动到超过数组的末尾。这个时候如果有空闲的空间,我们会把头或者尾跳到数组的头开始继续移动。所以添加元素并确定元素的下标是一个将元素下标值和数组长度进行求模运算的过程。addLast方法通过和当前数组长度减1求与运算来得到最新的下标值。它的效果相当于tail = (tail + 1) % elements.length;
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
addFirst和offerFirst是在head元素的之前插入元素,所以他们的位置为 (head - 1) & (elements.length - 1)。
取元素
获取元素主要包括如下几个方法:
public E element() {
return getFirst();
}
public E getFirst() {
E x = elements[head];
if (x == null)
throw new NoSuchElementException();
return x;
}
public E peek() {
return peekFirst();
}
public E peekFirst() {
return elements[head]; // elements[head] is null if deque empty
}
public E getLast() {
E x = elements[(tail - 1) & (elements.length - 1)];
if (x == null)
throw new NoSuchElementException();
return x;
}
public E peekLast() {
return elements[(tail - 1) & (elements.length - 1)];
}
这部分代码算是最简单的,无非就是取tail元素或者head元素的值。
关于队列的几种运算方法定义的特别杂乱,很容易让人搞混。如果从一个最简单的单向队列角度来看的话,我们可以把Queue中的enqueue方法对应到addLast方法,因为我们每次添加元素就是在队尾增加。deque方法则对应到removeFirst方法。虽然也可以用其他的方法来实现,不过具体的实现细节和他们基本上是一样的。
LinkedList
现在,我们在来看看LinkedList对应Queue的实现部分。在前面一篇文章中,已经讨论过LinkedList里面Node的结构。它本身包含元素值,prev、next两个引用。对链表的增加和删除元素的操作不像数组,不存在要考虑下标的问题,也不需要扩展数组空间,因此就简单了很多。先看查找元素部分:
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
这里唯一值得注意的一点就是last引用是指向队列最末尾和元素,和前面ArrayDeque的情况不一样。
添加元素的方法如下:
public boolean add(E e) {
linkLast(e);
return true;
}
public boolean offer(E e) {
return add(e);
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
linkLast的方法在前一篇文章里已经分析过,就不再重复。
删除元素的方法主要有remove():
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
这部分的代码看似比较长,实际上是遍历整个链表,如果找到要删除的元素,则移除该元素。这部分的难点在unlink方法里面。我们分别用要删除元素的前面和后面的引用来判断各种当prev和next为null时的各种情况。虽然不是很复杂,但是很繁琐。
总结
从我们实际中的考量来看,Queue和Deque他们本身不仅定义了作为一个队列需要的基本功能。同时因为队列也是属于整个集合类这一个大族里面的,所以他们也必须要具备集合类的一些常用功能,比如元素查找,删除,迭代器等。我们读一些集合类的代码时,尤其是一些接口的定义,会发现一个比较有意思的事情。就是通常一些子接口把父接口的方法又重新定义了一遍。这样似乎违背了面向对象里继承的原则。后来经过一些讨论,发现主要原因是一些jdk版本的更新,有的新类是后面新增加的。这些新的接口有的是为了保持兼容,有的是为了保证后续生成文档里方便用户知道它也有同样的功能而不需要再去查它的父类,就直接把父类的东西给搬过来了。比较有意思,读代码还读出点历史感了。
参考资料
http://docs.oracle.com/javase/7/docs/api/java/util/Queue.html
http://docs.oracle.com/javase/7/docs/api/java/util/Deque.html
http://blog.donews.com/maverick/archive/2005/08/31/534269.aspx
- 大小: 34.7 KB
- 大小: 8.6 KB
- 大小: 10.8 KB
分享到:
相关推荐
这个“一个讲解很清晰的Java集合框架PPT”显然是一个对外公开的教育资源,旨在帮助学习者深入理解Java集合的概念、结构以及实际应用。 在Java中,集合框架主要包括四大接口:List、Set、Queue和Map。每个接口都有...
以下是一些关于Java集合类的重要知识点: 1. **Collection与Collections的区别** - **Collection** 是所有单列集合的父接口,包括 List、Set 和 Queue。它定义了集合的基本操作,如增加元素、删除元素、遍历元素等...
本篇文章将深入探讨Java集合框架的各个方面,帮助开发者从基础到高级全面掌握这一关键知识。 首先,我们要理解Java集合框架的基础概念。集合是对象的容器,可以容纳多个对象,而框架则是一组接口和实现这些接口的类...
Java集合框架是Java编程语言中的一个核心部分,它为数据结构和对象的存储、管理和操作提供了统一...通过学习和分析`chapter3.html`这样的文档,我们可以深入了解每个集合类的内部工作原理,进一步提升我们的编程技能。
首先,Java集合框架包括List、Set、Queue等接口,以及ArrayList、LinkedList、HashSet、HashMap等实现类。这些接口和类提供了存储、检索、遍历和修改元素的方法。例如,ArrayList是以动态数组为基础的列表实现,适合...
5. **PowerDesigner格式文件**:`.oom` 文件是PowerDesigner创建的对象模型文件,这可能包含了更详细的类图和关系图,可以用来深入分析Java集合类的内部结构和实现细节,比如类的属性、方法、以及它们之间的关联。...
本章PDF课件详细讲解了这些概念,并可能包括实例代码演示、性能分析以及各种操作的注意事项,旨在帮助学习者深入理解并熟练运用Java集合框架。通过学习,开发者可以更高效地管理内存资源,优化程序性能,解决实际...
这个"深入理解Java集合框架.zip"文件很可能是包含了一系列关于Java集合框架的详细资料,比如源码分析、设计模式以及最佳实践。下面将详细探讨Java集合框架的关键知识点。 1. **集合接口**:Java集合框架的核心接口...
5. **集合框架**:Java集合框架包括List、Set、Queue和Map等接口,以及ArrayList、LinkedList、HashSet、HashMap等实现类。实例会展示如何存储、操作和遍历这些数据结构。 6. **IO流**:Java的IO流处理涵盖了读写...
Java集合框架包括接口(如List、Set、Queue)和实现类(如ArrayList、LinkedList、HashSet、HashMap等)。理解各种集合的区别,以及它们的实现方式和应用场景,是提升编程效率的关键。例如,List接口中的ArrayList和...
通过分析和实践"Java_jihe"压缩包中的源码,我们可以深入理解这些接口和类的内部工作原理,学习如何在实际项目中有效利用Java集合框架。这将极大地提升我们的编程能力和解决问题的效率。记住,不断地学习和实践是...
Java集合框架是面试中常考的部分,包括List、Set、Queue和Map接口,以及ArrayList、LinkedList、HashSet、HashMap等实现类: 1. 集合特性:线程安全、是否允许重复元素、排序规则等。 2. 遍历方式:迭代器、增强for...
描述中提到的链接指向了一个博客文章,尽管没有提供具体细节,但可以推测这篇文章可能提供了关于Java集合类的深入讨论,或者提供了与压缩包中的“Lesson6集合类.ppt”相关的补充材料。博主“Dean Kong”可能分享了他...
在面试中,对于Java集合的深入理解往往被视为衡量开发者能力的重要指标。本文将深入剖析Java集合的源码,探讨其内部实现机制,并结合常见面试题,帮助你更好地理解和应用这些知识。 首先,我们从基础开始,Java集合...
这篇博文链接虽然没有提供具体的内容,但是我们可以基于Java集合框架的常见知识点进行深入的解释。 首先,我们来了解一下Java集合框架的基本层次结构。在Java中,集合框架主要分为两大类:接口和实现。接口定义了...
在本文中,我们将全面分析Java集合框架的核心概念和实现细节。 首先,Java集合框架的基础是`Collection`接口,它是所有集合类的根接口,定义了集合的基本操作。`Collection`接口有两个重要的子接口:`List`和`Set`...
Java集合框架是Java语言提供的一组接口和类,用于存储和操作各种数据结构,如列表(List)、队列(Queue)、集(Set)和映射(Map)。然而,标准的集合框架并没有直接提供对树结构的支持,因此我们需要自己创建。 ...
总的来说,这个资料包是学习Java集合的宝贵资源,结合例题、源码分析和PPT教学,你可以深入理解Java集合的精髓,提升编程能力,为日后的开发工作打下坚实基础。无论是初学者还是有一定经验的开发者,都可以从中受益...
2. **集合框架**:Java集合框架包括List、Set、Queue等接口以及ArrayList、LinkedList、HashSet、HashMap等实现类。书中会深入解析这些数据结构的特性、性能和使用场景,还会介绍泛型、迭代器和流API的使用。 3. **...
4. **集合框架**:Java集合框架是数据结构和算法的Java实现,包括List、Set、Queue和Map等接口,以及ArrayList、LinkedList、HashSet、HashMap等具体实现。书籍将深入讲解这些容器的特性和使用场景,以及泛型、迭代...