您的位置:首页 > 理论基础 > 数据结构算法

Java 数据结构之 Queue(队列)

2013-02-22 11:08 573 查看
在JDK 1.5 中新增加了java.util.Queue接口,用以支持队列的常见操作。

public interface Queue<E>extends Collection<E>

队列除了基本的
Collection
操作外,队列还提供其他的插入、提取和检查操作。每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(null 或false,具体取决于操作)。插入操作的后一种形式是用于专门为有容量限制的Queue 实现设计的;在大多数实现中,插入操作不会失败。

抛出异常返回特殊值
插入
add(e)
offer(e)
移除
remove()
poll()
检查
element()
peek()
队列通常(但并非一定)以 FIFO(先进先出)的方式排序各个元素。不过优先级队列和 LIFO 队列(或堆栈)例外,前者根据提供的比较器或元素的自然顺序对元素进行排序,后者按 LIFO(后进先出)的方式对元素进行排序。无论使用哪种排序方式,队列的头 都是调用
remove()
poll()
所移除的元素。在 FIFO 队列中,所有的新元素都插入队列的末尾。其他种类的队列可能使用不同的元素放置规则。每个Queue
实现必须指定其顺序属性。

如果可能,
offer
方法可插入一个元素,否则返回false。这与
Collection.add
方法不同,该方法只能通过抛出未经检查的异常使添加元素失败。offer 方法设计用于正常的失败情况,而不是出现异常的情况,例如在容量固定(有界)的队列中。

remove()

poll()
方法可移除和返回队列的头。到底从队列中移除哪个元素是队列排序策略的功能,而该策略在各种实现中是不同的。remove() 和poll() 方法仅在队列为空时其行为有所不同:remove() 方法抛出一个异常,而poll() 方法则返回null。

element()

peek()
返回,但不移除,队列的头。 

Queue 实现通常不允许插入 null 元素,尽管某些实现(如
LinkedList
)并不禁止插入null。即使在允许 null 的实现中,也不应该将null 插入到
Queue 中,因为 null 也用作poll 方法的一个特殊返回值,表明队列不包含元素。 

JDK1.5中提供了Queue的子类,如下

All Known Implementing Classes:

AbstractQueue,
ArrayBlockingQueue, ArrayDeque,
ConcurrentLinkedQueue, DelayQueue,LinkedBlockingDeque,LinkedBlockingQueue,
LinkedList,
PriorityBlockingQueue,PriorityQueue,SynchronousQueue

AbstractQueue类

如你所见,java.util.LinkedList类实现了java.util.Queue接口,同样,AbstractQueue也是这样。AbstractQueue 类实现了java.util接口的一些方法(因此在它的名字中包含abstract)。而AbstractQueue将重点放在了实现offer,poll和peek方法上。另外使用一些已经提供的具体实现。

PriorityQueue类

在PriorityQueue中,当你添加元素到Queue中时,实现了自动排序。根据你使用的PriorityQueue的不同构造器,Queue元素的顺序要么基于他们的自然顺序要么通过PriorirtyQueue构造器传入的Comparator来确定。下面的代码示例了PirorityQueue类的使用方法。在Queue的前边是字符串"Alabama"-由于元素在PriorityQueue中是按自然顺序排列的(此例中是按字母表顺序)。

PriorityQueue<String> priorityQueue = new PriorityQueue<String>();

priorityQueue.offer("Texas");

priorityQueue.offer("Alabama");

priorityQueue.offer("California");

priorityQueue.offer("Rhode Island");

int queueSize = priorityQueue.size();

for (int i =0; i< queueSize; i++)

{

     System.out.println(priorityQueue.poll());

}

执行结果如下:

Alabama

California

Rhode Island

Texas

Queue各项按照自然顺序-字母顺序-来排列。

如上提到的,你可以创建你自己的Comparator类并提供给PirorityQueue。如此,你可以定义你自己的排序方式。在PriorityQueueComparatorUsageExample 类中可找到此方式,在其中使用了一个名为State的助手类。如你在下边看到的,在类定义中,State只简单地包含了一个名字和人口。

private String name;

private int population;

public State(String name, int population)

{

     super();

     this.name = name;

     this.population = population;

}

public String getName()

{

     return this.name;

}

public int getPopulation()

{

     return this.population;

}

public String toString()

{

     return getName() + " - " + getPopulation();

}

在PriorityQueueComparatorUsageExample中,Queue使用了java.util.Comparator的自定义实现来定义排列顺序(如下)。

PriorityQueue<State> priorityQueue = 

     new PriorityQueue(6, new Comparator<State>()

{

     public int compare(State a, State b)

     {

       System.out.println("Comparing Populations");

       int populationA = a.getPopulation();

       int populationB = b.getPopulation();

       if (populationB>populationA)

            return 1;

       else if (populationB<populationA)

            return -1;

       else 

            return 0; 

     }

}

);

执行PriorityQueueComparatorUsageExample类后,添加到Queue中的State对象将按人口数量排放(从低到高)。

 
阻塞Queue

Queue通常限定于给定大小。迄今为止,通过Queue的实现你已经看到,使用offer或add方法enqueue Queue(并用remove或poll来dequeue Queue)都是假设如果Queue不能提供添加或移除操作,那么你不需要等待程序执行。java.util.concurrent.BlockingQueue接口实现阻塞。它添加了put和take方法。举一个例子可能更有用。

使用原来的producer/consumer关系来假定你的producer写一个Queue(更特定是一个BlockingQueue)。你有一些consumer正从Queue中读取,在一个有序的方式下,哪种方式是你希望看到的。基本上,每个consumer需要等待先于它并获准从Queue中提取项目的前一个consumer。用程序构建此结构,先生成一个producer线程用于向一个Queue中写数据,然后生成一些consumer线程从同一Queue中读取数据。注意,线程会阻塞另一线程直到当前线程做完从Queue中提取一项的操作。

下面的代码展示了类Producer写BlockingQueue的过程。注意run方法中的对象(你有责任实现,因为你继承了Thread)在等待了随机数量的时间(范围从100到500毫秒)后,被放进了BlockingQueue。放到Queue中的对象只是一些包含消息产生时的时间的字符串。

添加对象的实际工作是由如下语句实现的: 

blockingQueue.put("Enqueued at: " + time)

put方法会抛出InterruptedException,因此,put操作需要被try...catch块包围,用来捕获被抛出的异常(见Listing 1)。

从producer中提取消息的是Consumer对象,它也继承自Thread对象并因此要实现run方法(见Listing 2)。

Consumer类在设计上是类似于Producer类的。Consumer类使用take方法去从Queue中取出(即dequeue)消息,而不是将消息放到BlockingQueue中。如前所述,这需要等待到有什么内容确实存在于Queue中时才发生。如果producer线程停止放置(即enqueue)对象到Queue中,那么consumer将等待到Queue的项目有效为止。下面所示的TestBlockingQueue类,产生四个consumer线程,它们从BlockingQueue中尝试提取对象。

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.LinkedBlockingQueue;

public class TestBlockingQueue

{

  public static void main(String args[])

  {

    BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<String>();

    Producer producer = new Producer(blockingQueue, System.out);

    Consumer consumerA = new Consumer("ConsumerA", blockingQueue, System.out);

    Consumer consumerB = new Consumer("ConsumerB", blockingQueue, System.out);

    Consumer consumerC = new Consumer("ConsumerC", blockingQueue, System.out);

    Consumer consumerD = new Consumer("ConsumerD", blockingQueue, System.out);

    producer.start();

    consumerA.start();

    consumerB.start();

    consumerC.start();

    consumerD.start();  

  }

}
 

Figure 1. Consumer Threads: These threads dequeue messages from the BlockingQueue in the order that you spawned them.

下面一行创建BlockingQueue:

BlockingQueue<String> blockingQueue 

                    = new LinkedBlockingQueue<String>();

注意,它使用BlockingQueue的LinkedBlockingQueue实现。这是因为BlockingQueue是一个抽象类,你不能直接实例化它。你也可以使用ArrayBlockingQueueQueue类型。ArrayBlockingQueue使用一个数组作为它的存储设备,而LinkedBlockingQueue使用一个LinkedList。ArrayBlockingQueue的容量是固定的。对于LinkedBlockingQueue,最大值可以指定;默认是无边界的。本示例代码采用无边界方式。

在类的执行期间,从Queue中读取对象以顺序方式执行(见下面例子的执行)。实际上,一个consumer线程阻塞其他访问BlockingQueue的线程直到它可以从Queue中取出一个对象。

 
DelayQueue-我是/不是不完整的

在某些情况下,存放在Queue中的对象,在它们准备被取出之前,会需要被放在另一Queue中一段时间。这时你可使用java.util.concurrent.DelayQueue类,他实现类BlockingQueue接口。DelayQueue需要Queue对象被驻留在Queue上一段指定时间。

我想用来证实它的现实例子(这可能是你非常渴望的)是关于松饼(muffins)。噢,Muffin对象(象我们正在谈论的Java-没有coffee双关意图)。假定你有一个DelayQueue并在其中放了一些Muffin对象。Muffin对象(如下所示)必须实现java.util.concurrent.Delayed接口,以便可被放在DelayQueue中。这个接口需要Muffin对象实现getDelay方法(如下所示)。getDelay方法,实际上声明给多长时间让对象保存在DelayQueue中。当该方法返回的值变为0或小于0时,对象就准备完毕(或在本例子中,是烤制完毕)并允许被取出(见Listing
3)。

Muffin类也实现compareTo(java.util.concurrent.Delayed)方法。由于Delayed接口继承自java.lang.Comparable类,这通过约定限制你要实现Muffin对象的bakeCompletion时间。

由于你不是真想去吃没有完全烤熟的Muffin,因此,需要将Muffin放在DelayQueue中存放推荐的烤制时间。Listing 4,取自DelayQueueUsageExample类,展示了从DelayQueue中enqueue和dequeue Muffin对象。

如你所见,对Muffin对象的烤制时间是使用它的构造器设置的(构造器期望烤制时间是以秒计)。

如前所讲,Muffin对象放到DelayQueue中是不允许被取出的,直到他的延时时间(又叫烤制时间)超期。元素被从Queue中取出基于最早的延时时间。在本例中,如果你有一些已经烤过的Muffin对象,他们将按他们已经等待多久而被取出(换句话说,最早被烤制的Muffin会在新烤制的Muffin之前被取出)。

SynchronousQueue

在Java 1.5中,另外一种阻塞Queue实现是SynchronousQueue。相当有趣的是,该Queue没有内在容量。这是故意的,因为Queue意在用于传递目的。这意味着,在一个同步Queue结构中,put请求必须等待来自另一线程的给SynchronousQueue的take请求。同时,一个take请求必须等待一个来自另一线程的给SynchronousQueue的put请求。用程序来示例此概念,可参见示例代码。类似于前边的LinkedBlockingQueue例子,它包含一个consumer(SynchConsumer),见Listing
5。

Listing 5中的代码使用SynchronousQueue类的poll(long timeout,TimeUnit unit)方法。此方法允许poll过程在厌倦等待另一消费线程写SynchronousQueue之前等待一个指定时间(本例中是20秒)。

在Listing 6中的producer(SynchProducer)使用相似的offer(E o,long timeout, TimeUnit unit)方法去放置对象到SynchronousQueue中。使用此方法允许在厌倦等待另一线程去读取SynchronousQueue之前等待一段时间(本例中为10秒)。

TestSynchQueue 展示了producer和consumer的动作:

import java.util.concurrent.SynchronousQueue;

import java.util.concurrent.LinkedBlockingQueue;

public class TestSynchQueue

{

  public static void main(String args[])

  {

    SynchronousQueue<String> synchQueue = new SynchronousQueue<String>();

    SynchProducer producer = new SynchProducer("ProducerA",synchQueue, System.out);

    SynchConsumer consumerA = new SynchConsumer("ConsumerA", synchQueue, System.out);

    consumerA.start();

    producer.start();

  }

}

当试图明白隐藏在SynchronousQueue后面的概念时,要牢记这些Queue通常被使用在什么地方。JavaDoc中关于同步Queue指出:

"它们[同步Queue]是适合于传递设计,在那里运行在一个线程中的对象必须与运行在另外一个线程中的对象同步以便于交给它一些信息,时间或任务。"
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  队列 数据结构 queue