SwingWorker
Today I discovered Java 6 SwingWorker. Swing is traditionally a bit tricky to work with when dealing with multi-threading, and though SwingUtilities.invokeLater
(or its equivalent EventQueue.invokeLater
, the former calling the latter) was good enough, you felt you were a bit on your on your own when it came to implement some task triggered by a button.
SwingWorker provides all the nuts and bolts for performing lengthy tasks, whilst keeping the UI responsive. Here is a simple example of how to use it, directly inspired by Project Euler #7, “find the 10001th prime number” which in turn I remembered thanks to the Javadoc description of SwingWorker
(Project Euler always comes very handy when trying to find decent examples).
First, here is the task class that will perform this job, called PrimeNumberTask
import java.util.ArrayList; import java.util.List; import java.util.Observer; import javax.swing.JOptionPane; import javax.swing.SwingWorker; public class PrimeNumbersTask extends SwingWorker, Integer> { private int numbersToFind; private int numbersFound; private List
primeObservers = new ArrayList (); private int lastPrime; PrimeNumbersTask(int numbersToFind) { this.numbersToFind = numbersToFind; } @Override protected List doInBackground() throws Exception { List primes = new ArrayList (); int current = 1; while (numbersFound < numbersToFind && !isCancelled()) { if (isPrime(current)) { numbersFound++; primes.add(current); notify(current); } if (current < 3) { current++; } else { current += 2; } } lastPrime = primes.get(primes.size() - 1); return primes; } public void addObserver(Observer observer) { primeObservers.add(observer); } protected void notify(Integer newPrime) { for (Observer o : primeObservers) { o.update(null, newPrime); } } private boolean isPrime(double n) { if (n == 1) { return false; } else if (n < 4) { return true; // 2 and 3 are prime. } else if (n % 2 == 0) { return false; } else if (n < 9) { return true; // 4, 6 and 8 already excluded. } else if (n % 3 == 0) { return false; } else { double r = Math.floor(Math.sqrt(n)); double f = 5; while (f <= r) { if (n % f == 0) { return false; } if (n % (f + 2) == 0) { return false; } f += 6; } } return true; } @Override protected void done() { JOptionPane.showMessageDialog(null, numbersToFind + "st prime is " + lastPrime); } }
In this task, nothing overly exciting. We implement doInBackground
, which is our “lengthy task.” This job notifies listeners every time a prime is found: this will be handy for displaying new primes, and updating a JProgressBar
.
The frame is a simple Swing UI whose job is to create the interface component (a text area displaying prime numbers, a progress bar, and a button triggering the task), and the action instantiating the task and starting it. It is also responsible for creating the Observer
s and registering it with the SwingWorker
:
import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.util.Observable; import java.util.Observer; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JProgressBar; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.SwingUtilities; public class LongJobFrame extends JFrame { public final static int NUMBER_OF_PRIMES = 10001; JProgressBar progressBar = new JProgressBar(0, NUMBER_OF_PRIMES); JTextArea textArea = new JTextArea(); public LongJobFrame() { JButton startButton = new JButton(new LongJobAction()); add(BorderLayout.SOUTH, startButton); add(BorderLayout.NORTH, progressBar); add(BorderLayout.CENTER, new JScrollPane(textArea)); setSize(600, 450); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); } class LongJobAction extends AbstractAction { public LongJobAction() { putValue(AbstractAction.NAME, "Start"); } @Override public void actionPerformed(ActionEvent e) { PrimeNumbersTask task = new PrimeNumbersTask(NUMBER_OF_PRIMES); task.addObserver(new Observer() { public void update(Observable o, Object arg) { progressBar.setValue(progressBar.getValue() + 1); } }); task.addObserver(new Observer() { public void update(Observable o, Object arg) { StringBuffer contentBuffer = new StringBuffer(); if (textArea.getText().length() > 0) { contentBuffer.append(textArea.getText()); contentBuffer.append(System.getProperty("line.separator")); } contentBuffer.append(arg); textArea.setText(contentBuffer.toString()); } }); task.execute(); } } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { new LongJobFrame(); } }); } }
Interestingly enough, you'll notice that I have defined “half” of the Observer
pattern, leaving out the Observable
. Here it is definitely not useful, as we are maintaining our own list of Observer
s, saving us the headache of trying to subclass Observable
(since our task is already subclassing SwingWorker
).
There is also another way to achieve this by using the setProgress
method in SwingWorker
which triggers a PropertyChangeEvent
progress, and all you have to do is register PropertyChangeListener
with the Swing worker. The small issue with this is that setProgress
only takes a value between 0 and 100 (which would probably be ok for our progress bar), and there is no way of passing a specific object to the listeners like the new found prime, so this means either SwingWorker
has to hold a reference to the Swing components to update them (I want to avoid this coupling), or SwingWorker
has to hold a instance variable that can be read when the listeners are notified (which has no guarantee of working, as this notification happens asynchronously).