Core Java Technologies Tech Tips Tips, Techniques, and Sample Code Welcome to the Core Java Technologies Tech Tips for October 19, 2004. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE). This issue covers: * Queues and Delayed Processing * Getting to Know Synth These tips were developed using Java 2 Platform Standard Edition Development Kit 5.0 (JDK 5.0). You can download JDK 5.0 at http://java.sun.com/j2se/1.5.0/download.jsp. This issue of the Core Java Technologies Tech Tips is written by John Zukowski, president of JZ Ventures, Inc. (http://www.jzventures.com). You can view this issue of the Tech Tips on the Web at http://java.sun.com/developer/JDCTechTips/2004/tt1019.html See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms. For more Java technology content, visit these sites: java.sun.com - The latest Java platform releases, tutorials, and newsletters. java.net - A web forum for collaborating and building solutions together. java.com - The marketplace for Java technology, applications and services. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - QUEUES AND DELAYED PROCESSING The J2SE 5.0 release adds a new top-level Queue interface to the Collections Framework, to go along with the existing Map, List, and Set interfaces. Typically, queues are a first in, first out data structure, though for some implementations such as priority queues, elements might not be added at the end of a queue. You can think of a queue and its first in, first out structure much like the line at a bank. A bank teller checks to see if there are customers in the line. The teller then handles the first customer in the line, processing any transactions for that person. In doing so, the teller removes the customer from the waiting line. In addition to the new Queue interface in J2SE 5.0,there are some new queue implementations. One such implementation is a DelayQueue. In a DelayQueue, the items in the queue can't be processed until a delay has happened. In this tip, you'll learn about the new Queue interface and the DelayQueue implementation. First, let's examine the Queue interface. This interface extends Collection and adds its own series of five methods: o E element() o boolean offer(E o) o E peek() o E poll() o E remove() As a result of the new support in J2SE 5.0 for generics (http://java.sun.com/j2se/1.5.0/docs/guide/language/generics.html), E here can be any data type, as specified by the type of element defined when the Queue got created. Although you can certainly use the Collection interface methods for adding and removing elements from a Queue, it is recommended that you limit yourself to those in the Queue interface. That's because the methods enforce additional behavioral requirements. For instance, instead of adding an element to the queue with the Collection.add method, you offer it to the queue with the offer method. Why the difference? An add operation can fail -- one example is if a queue is bounded in size (using the bank line analogy, there can't be more than 10 people waiting.) With the add method of Collection, add fails by throwing an exception. By comparison, the offer method "fails" by returning false. So by using the offer method, you can save exception handling for truly exceptional cases (especially since the exception thrown is an unchecked runtime exception). The other four methods of Queue can be described in pairs: remove/poll and element/peek. Both the remove and poll methods of Queue are used to remove the first element, or "head", of the queue. The remove method throws an exception when it's called with an empty collection. The poll method simply returns null in this case. Instead of removing the head element, you can just look at the element. That's where the element and peek methods are used. Here, the element method throws an exception on an empty queue, and peek returns null. Because queues are typically used for processing tasks, having an empty queue isn't necessarily an exceptional condition. As a result, the poll/peek model tends to be the more appropriate model to follow. Queue is typically used in a way similar to the following: class Producer implements Runnable { private final Queue queue; Producer(Queue q) { queue = q; } public void run() { try { while(true) { queue.offer(produce()); } } catch (InterruptedException ex) { ... handle ...} } Object produce() { ... } } class Consumer implements Runnable { private final Queue queue; Consumer(Queue q) { queue = q; } public void run() { try { Object o; while((o = queue.poll()) != null) { consume(o); } } catch (InterruptedException ex) { ... handle ...} } void consume(Object x) { ... } } One question you might have is what happens when the queue is full (for the producer) or empty (for the consumer)? That question is a good reason to start the discussion of another new queue interface: BlockingQueue. Instead of using Queue to blindly add elements (with offer) and remove them (with poll), you can add elements with the put method of BlockingQueue and remove them with the take method. Both put and take cause the calling thread to block if a certain condition is true. For put, the blocking condition is a full queue. For take, the blocking condition is an empty queue. The typical usage pattern for BlockingQueue is: class Producer implements Runnable { private final BlockingQueue queue; Producer(BlockingQueue q) { queue = q; } public void run() { try { while(true) { queue.put(produce()); } } catch (InterruptedException ex) { ... handle ...} } Object produce() { ... } } class Consumer implements Runnable { private final BlockingQueue queue; Consumer(BlockingQueue q) { queue = q; } public void run() { try { while(true) { consume(queue.take()); } } catch (InterruptedException ex) { ... handle ...} } void consume(Object x) { ... } } Each created producer waits if put tries to add a newly produced item to a full queue, and take waits until there is something to take. The while(true) condition will never change if the queue is empty. DelayQueue is a concrete implementation of the BlockingQueue interface. Items added to a DelayQueue must implement the new Delayed interface, which has a single method: long getDelay(TimeUnit unit). DelayQueue works as a time-based scheduling queue that is backed by a priority heap data structure. In other words, when you add an element to the queue, you specify how long the queue must wait before the element can be processed. To demonstrate, the following program, DelayTest, provides an implementation of the Delayed interface that works in seconds. The key things to know are (1) a nanosecond is a billionth of a second, and (2) there is a new method of System called nanoTime that allows you to work in nanosecond units. Working in nanoseconds is important because the getDelay method wants the number of nanoseconds returned. import java.util.Random; import java.util.concurrent.Delayed; import java.util.concurrent.DelayQueue; import java.util.concurrent.TimeUnit; public class DelayTest { public static long BILLION = 1000000000; static class SecondsDelayed implements Delayed { long trigger; String name; SecondsDelayed(String name, long i) { this.name = name; trigger = System.nanoTime() + (i * BILLION); } public int compareTo(Delayed d) { long i = trigger; long j = ((SecondsDelayed)d).trigger; int returnValue; if (i < j) { returnValue = -1; } else if (i > j) { returnValue = 1; } else { returnValue = 0; } return returnValue; } public boolean equals(Object other) { return ((SecondsDelayed)other).trigger == trigger; } public long getDelay(TimeUnit unit) { long n = trigger - System.nanoTime(); return unit.convert(n, TimeUnit.NANOSECONDS); } public long getTriggerTime() { return trigger; } public String getName() { return name; } public String toString() { return name + " / " + String.valueOf(trigger); } } public static void main(String args[]) throws InterruptedException { Random random = new Random(); DelayQueue queue = new DelayQueue(); for (int i=0; i < 10; i++) { int delay = random.nextInt(10); System.out.println("Delaying: " + delay + " for loop " + i); queue.add(new SecondsDelayed("loop " + i, delay)); } long last = 0; for (int i=0; i < 10; i++) { SecondsDelayed delay = (SecondsDelayed)(queue.take()); String name = delay.getName(); long tt = delay.getTriggerTime(); if (i != 0) { System.out.println("Delta: " + (tt - last) / (double)BILLION); } System.out.println(name + " / Trigger time: " + tt); last = tt; } } } The DelayTest program places ten elements in the DelayQueue before processing them. Here is the output for one run of DelayQueue: Delaying: 8 for loop 0 Delaying: 7 for loop 1 Delaying: 2 for loop 2 Delaying: 4 for loop 3 Delaying: 0 for loop 4 Delaying: 9 for loop 5 Delaying: 3 for loop 6 Delaying: 4 for loop 7 Delaying: 6 for loop 8 Delaying: 2 for loop 9 loop 4 / Trigger time: 1883173869520000 Delta: 1.9995545 loop 2 / Trigger time: 1883175869074500 Delta: 0.0012475 loop 9 / Trigger time: 1883175870322000 Delta: 0.9995177 loop 6 / Trigger time: 1883176869839700 Delta: 0.9995187 loop 3 / Trigger time: 1883177869358400 Delta: 6.408E-4 loop 7 / Trigger time: 1883177869999200 Delta: 2.0001667 loop 8 / Trigger time: 1883179870165900 Delta: 0.9986953 loop 1 / Trigger time: 1883180868861200 Delta: 0.9995595 loop 0 / Trigger time: 1883181868420700 Delta: 1.001262 loop 5 / Trigger time: 1883182869682700 The output indicates that the item for loop 4 is set to start at time 0, that is, without delay: Delaying: 0 for loop 4 So it runs first: loop 4 / Trigger time: 1883173869520000 Loop 2 has a 2 second delay: Delaying: 2 for loop 2 So it comes in next: loop 2 / Trigger time: 1883175869074500 The delta here is 1.9995545, which is roughly 2 seconds: Delta: 1.9995545 Similar deltas are there for the other loops. For a more real-world example, instead of just printing out something when pulled from the DelayQueue, items in the queue might implement Runnable and you would call their run method. For additional information about Queue, DelayQueue, and other changes to the Collections Framework with J2SE 5.0, see the document "Collection Framework Enhancements" (http://java.sun.com/j2se/1.5.0/docs/guide/collections/changes5.html). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - GETTING TO KNOW SYNTH Synth is a new look and feel added to project Swing in J2SE 5.0. Synth is a skinnable (that is, a customizable) look and feel, where the "skin" (that is, the user interface) is controlled by an XML file. Instead of customizing the look and feel by providing default properties to the UIManager in a Properties table, you load in an XML file with component definitions. That means you can create a custom look without writing code. To demonstrate, here's an example of using the Synth look and feel: import java.awt.BorderLayout; import java.awt.EventQueue; import java.io.InputStream; import java.text.ParseException; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JFrame; import static javax.swing.JFrame.*; import javax.swing.UIManager; import javax.swing.plaf.synth.SynthLookAndFeel; public class HelloSynth { public static void main(String args[]) { Runnable runner = new Runnable() { public void run() { SynthLookAndFeel synth = new SynthLookAndFeel(); try { Class aClass = HelloSynth.class; InputStream is = aClass.getResourceAsStream("synth.xml"); if (is == null) { System.err.println("Missing configuration file"); System.exit(-1); } synth.load(is, aClass); } catch (ParseException e) { System.err.println("Bad configuration file"); System.exit(-2); } try { UIManager.setLookAndFeel(synth); } catch (javax.swing.UnsupportedLookAndFeelException e) { System.err.println("Cannot change to Synth"); System.exit(-3); } JFrame frame = new JFrame("First"); frame.setDefaultCloseOperation(EXIT_ON_CLOSE); JLabel label = new JLabel("Hello, Synth"); label.setHorizontalAlignment(JLabel.CENTER); frame.add(label); frame.setSize(300, 100); frame.setVisible(true); } }; EventQueue.invokeLater(runner); } } The HelloSynth program creates a new SynthLookAndFeel object and loads an XML file, synth.xml, with synth.load(InputStream, Class). This means that you need the XML file to complete this first example: Run the HelloSynth program with the synth.xml file. The program reads in the XML file, creates a JLabel, and adds it to the screen. Notice the absence of any color or font configuring of the label in the program. Instead, that's done through the XML file. The synth.xml file configures the label as follows: 18-point bold-italic font, opaque, foreground color of pink (text color), and background color of yellow. The HelloSynth program then draws the label according to those specifications. java HelloSynth synth.xml If you don't like that color-font combination, you can change the information in the XML file to produce something more appealing. For example, you might change the font to bold, the text color to red, and the background to black: By simply changing the XML file, and without recompiling your program, you change the look produced by the program. java HelloSynth synth.xml And that is the power of the Synth look and feel. It allows you to change the look produced by the program without reprogramming. The format of the XML file used with Synth is described in the Synth File Format document (http://java.sun.com/j2se/1.5.0/docs/api/javax/swing/plaf/synth/doc-files/synthFileFormat.html). Inside the outermost synth tag, you can have tags such as The