Please note that this page has moved to: http://www.javacreed.com/swing-worker-example/.
Java provides a neat way to carry out long lasting jobs without have to worry about threads and hanging the application. It's called SwingWorker. It's not the latest thing on Earth (released with Java 1.6) and you may have already read about it. What I never came across was a practical example of the swing worker.
Swing Worker
SwingWorker is an abstract class which hides the threading complexities from the developer. It's an excellent candidate for applications that are required to do tasks (such as retrieving information over the network/internet) which may take some time to finish. It's ideal to detach such tasks from the application and simply keep an eye on their progress. But before we hit the road and start working with the swing worker we have to see what "eye" are we going to put on our worker so to say.
The following example illustrates a simple empty worker that will return/evaluate to an integer when the given task is finished. It will inform the application (the "eye" thing) with what's happening using objects of type string, basically text messages.
import javax.swing.SwingWorker;
public class MyBlankWorker extends SwingWorker<Integer, String> {
// Some code must go here
}
The string worker class provides two place holders (generics). The first one represents the type of object returned when the worker has finished working. The second one represents the type of information that the worker will use to inform (update) the application with its progress. The swing worker class also provides means to update the progress by means of an integer which has nothing to do with the two generics mentioned before.
Practical Example
Example: Let say we need to find the number of occurrences of a given word with in some documents. So we would be writing something like:
import java.io.File;
import javax.swing.SwingWorker;
public class SearchForWordWorker
extends SwingWorker<Integer, String> {
private final String word;
private final File[] documents;
public SearchForWordWorker(String word, File[] documents){
this.word = word;
this.documents = documents;
}
@Override
protected Integer doInBackground() throws Exception {
int matches = 0;
for(int i=0, size=documents.length; i<size; i++){
// Update the status: the keep an eye on thing
publish("Searching file: "+documents[i]);
try {
// Do the search stuff
// Here you increment the variable matches
} finally {
// Close the current file
}
// update the progress
setProgress( (i+1) * 100 / size);
}
return matches;
}
}
The first thing that comes into mind is where is the text "Searching file: ..." is going? The swing worker class provides another method called process which accepts a list (of type string in our case) and used to process the published information (which can be an object of any kind). Overriding this method, allows us to take full control of this information.
@Override
protected void process(List<String> chunks){
for(String message : chunks){
System.out.println(message);
}
}
The above example is not much useful. We may need to update the status bar of an application or the text of the progress bar or a label sitting somewhere in the application. Since we may be monitoring this worker from various UI components, ideally we add a level of isolation between the worker and the UI components. In many examples, the worker was fed UI components as constructor parameters. I would go for interfaces instead to make the design pluggable when possible.
In other occasions a worker may be used to populate a table which information is coming from a slow source. In this case we may use the table model as one of the workers parameter and the array of objects representing the row. The following example makes use of the default table model as the design is simpler.
import java.util.List;
import javax.swing.SwingWorker;
import javax.swing.table.DefaultTableModel;
public class PopulateTableWorker
extends SwingWorker<DefaultTableModel, Object[]> {
private final DefaultTableModel model;
public PopulateTableWorker(DefaultTableModel model){
this.model = model;
}
@Override
protected DefaultTableModel doInBackground() throws Exception {
// While there are more rows
while(Math.random() < 0.5){
// Get the row from the slow source
Object[] row = {1, "Java"};
Thread.sleep(2000);
// Update the model with the new row
publish(row);
}
return model;
}
@Override
protected void process(List<Object[]> chunks){
for(Object[] row : chunks){
model.addRow(row);
}
}
}
Note that the table model interface does not support addition of new rows. Alternatively we can make use of the table model interface. The model must be able to add a new row when this is required as otherwise an exception may be thrown.
import java.util.List;
import javax.swing.SwingWorker;
import javax.swing.table.TableModel;
public class PopulateTableWorker
extends SwingWorker<TableModel, Object[]> {
private final TableModel model;
private int rowIndex = 0;
public PopulateTableWorker(TableModel model){
this.model = model;
}
@Override
protected TableModel doInBackground() throws Exception {
// While there are more rows
while(Math.random() < 0.5){
// Get the row from the slow source
Object[] row = {1, "Java"};
Thread.sleep(2000);
// Update the model with the new row
publish(row);
}
return model;
}
@Override
protected void process(List<Object[]> chunks){
for(Object[] row : chunks){
for(int columnIndex=0, size=row.length;
columnIndex<size;
columnIndex++){
model.setValueAt(row[columnIndex], rowIndex, columnIndex);
}
}
rowIndex++;
}
}
Back to our example, we can have an interface called Informable (or you can pick a name of your liking) with one method: messageChanged(String message), which will be invoked whenever some progress is made and published.
public interface Informable {
void messageChanged(String message);
}
The worker also requires an instance of the interface as it will be used to publish the results through.
import java.io.File;
import java.util.List;
import javax.swing.SwingWorker;
public class SearchForWordWorker
extends SwingWorker<Integer, String> {
private final String word;
private final File[] documents;
private final Informable informable;
public SearchForWordWorker(String word,
File[] documents,
Informable informable){
this.word = word;
this.documents = documents;
this.informable = informable;
}
@Override
protected Integer doInBackground() throws Exception {
int matches = 0;
for(int i=0, size=documents.length; i<size; i++){
// Update the status: the keep an eye on thing
publish("Searching file: "+documents[i]);
try {
// Do the search stuff
// Here you increment the variable matches
} finally {
// Close the current file
}
// update the progress
setProgress( (i+1) * 100 / size);
}
return matches;
}
@Override
protected void process(List<String> chunks){
for(String message : chunks){
informable.messageChanged(message);
}
}
}
The above worker can be easily plugged into the application as show in the following example. This application has three graphical components: a label acting as a title displaying the latest message published by the worker; a text area which displays all messages published by the worker; and a progress bar showing the progress made.
The label and text area are governed by Informable interface. The progress bar is governed by the worker's progress property.
import java.awt.*;
import java.beans.*;
import java.io.*;
import javax.swing.*;
public class Application extends JFrame {
// The UI Components
private JLabel label;
private JProgressBar progressBar;
private JTextArea textArea;
private void initComponents(){
// The interface will update the text of the UI components
Informable informable = new Informable(){
@Override
public void messageChanged(String message){
label.setText(message);
textArea.append(message + "\n");
}
};
// The UI components
label = new JLabel("");
add(label, BorderLayout.NORTH);
textArea = new JTextArea(5, 30);
add(new JScrollPane(textArea), BorderLayout.CENTER);
progressBar = new JProgressBar();
progressBar.setStringPainted(true);
add(progressBar, BorderLayout.SOUTH);
// The worker parameters
String word = "hello";
File[] documents = {new File("Application.java"),
new File("Informable.java"),
new File("SearchForWordWorker.java")};
// The worker
SearchForWordWorker worker =
new SearchForWordWorker(word, documents, informable){
// This method is invoked when the worker is finished
// its task
@Override
protected void done(){
try {
// Get the number of matches. Note that the
// method get will throw any exception thrown
// during the execution of the worker.
int matches = get();
label.setText("Found: "+matches);
textArea.append("Done\n");
textArea.append("Matches Found: "+matches+"\n");
progressBar.setVisible(false);
}catch(Exception e){
JOptionPane.showMessageDialog(Application.this,
"Error", "Search",
JOptionPane.ERROR_MESSAGE);
}
}
};
// A property listener used to update the progress bar
PropertyChangeListener listener =
new PropertyChangeListener(){
public void propertyChange(PropertyChangeEvent event) {
if ("progress".equals(event.getPropertyName())) {
progressBar.setValue( (Integer)event.getNewValue() );
}
}
};
worker.addPropertyChangeListener(listener);
// Start the worker. Note that control is
// returned immediately
worker.execute();
}
// The main method
public static void main(String[] args){
SwingUtilities.invokeLater(new Runnable(){
public void run(){
Application app = new Application();
app.initComponents();
app.setDefaultCloseOperation(EXIT_ON_CLOSE);
app.pack();
app.setVisible(true);
}
});
}
}
The above example builds the application and kicks off the worker. The worker updates the application through the Informable instance. While the worker is performing it task, the application can carry on doing other things (event listening and painting) without hanging.
Cancel the Worker
Can we cancel the task? Yes. The worker can be stopped or better cancelled. The worker provides a method called cancel which accepts a parameter of type boolean. This parameter determines whether or not the worker should be waked up should it be found sleeping.
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
import javax.swing.*;
public class Application extends JFrame {
private JLabel label;
private JProgressBar progressBar;
private JTextArea textArea;
private JButton button;
private SearchForWordWorker worker;
private void initComponents(){
// The interface will update the text of the UI components
Informable informable = new Informable(){
@Override
public void messageChanged(String message){
label.setText(message);
textArea.append(message + "\n");
}
};
// The UI components
label = new JLabel("");
add(label, BorderLayout.NORTH);
textArea = new JTextArea(5, 30);
add(new JScrollPane(textArea), BorderLayout.CENTER);
// The cancel button
button = new JButton("STOP");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
// Cancel the worker and wake it up should it be sleeping
worker.cancel(true);
}
});
add(button, BorderLayout.EAST);
progressBar = new JProgressBar();
progressBar.setStringPainted(true);
add(progressBar, BorderLayout.SOUTH);
// The worker parameters
String word = "hello";
File[] documents = {new File("Application.java"),
new File("Informable.java"),
new File("SearchForWordWorker.java")};
// The worker
worker = new SearchForWordWorker(word, documents, informable){
@Override
protected void done(){
try {
int matches = get();
label.setText("Found: "+matches);
textArea.append("Done\n");
textArea.append("Matches Found: "+matches+"\n");
progressBar.setVisible(false);
}catch(Exception e){
JOptionPane.showMessageDialog(Application.this,
"Error", "Search",
JOptionPane.ERROR_MESSAGE);
}
}
};
// A property listener used to update the progress bar
PropertyChangeListener listener =
new PropertyChangeListener(){
public void propertyChange(PropertyChangeEvent event) {
if ("progress".equals(event.getPropertyName())) {
progressBar.setValue( (Integer)event.getNewValue() );
}
}
};
worker.addPropertyChangeListener(listener);
// Start the worker. Note that control is
// returned immediately
worker.execute();
}
public static void main(String[] args){
SwingUtilities.invokeLater(new Runnable(){
public void run(){
Application app = new Application();
app.initComponents();
app.setDefaultCloseOperation(EXIT_ON_CLOSE);
app.pack();
app.setVisible(true);
}
});
}
}
This will cause the worker's get method to throw the exception CancellationException to indicate that the worker was forced cancellation.
Conclusion
One thing to take from this article is: when possible do not refer to UI components directly from the worker. The worker is better viewed as the subject of the observer pattern. Provide an interface which the worker will use to communicate with its owner (application).
Thanks for the write up that was great. I'm currently using swingworker in my project. What is your opinion about threads in general in regards to time it takes to create a thread. I did some timing tests where a lot of time was spent in thread creation.
ReplyDeleteWelcome. I love to put some more articles but I never find the time...
ReplyDeleteThreads are very dependent on the environment. Both OS and load of the system may effect the outcome of the threads. Yes, like any other process, threads consume resources - these don't come for free. But when properly used, these can speed up the performance especially in parallel processing.
An example of threads is the GC. It employs some threads. GC will kick in depending on some parameters: such as thread priority and memory availability. Being a low priority thread, it may never be invoked if the system is very busy handling higher priority threads.
Hope this help.
Hi Albert,
ReplyDeleteThanks for the example, and I would appreciate if you could answer my question. Is there any particular reason for using Thread.sleep(2000) in the doInBackground() method?
The reason I am asking you this question is that I used a while loop in my doInBackground() to load vector and used publish(Vector) to work on the objects contained in the vector in the process(List) method. I used publish() in the if statement, to make sure that it gets to the publish() only if the vector is not empty. But for some strange reason, it always skipped the last valid vector and sent the empty vector to the process(). So I put some print statements in my while loop to see what's going on in there, and to my surprise it worked some of the times after that.
To make a long story short, thinking it could be a timing issue, I removed all the debug statements and used Thread.sleep(100) to slow it
down a little, and viola! it worked every time after that, but I still don't know why, and I am wondering whether it will work all the time in future or not?
Mehta
Hi Mehta,
ReplyDeleteMy Thread.sleep() is there to simulate a delay. I have a simple example and without it, it would run so fast that you'll see nothing. That's why I added the sleep there.
As for your problem; synchronization may be your answer. If you want, email me your code and I will try to have a look. My email is albertattard@gmail.com
Albert
Hi, Albert,
ReplyDeleteThanks for your post. As for your SearchForWordWorker example, if it gets canceled, does it guarantee to close the files it opened? That is, is the finally{} block in doInBackground() guaranteed to run?
Thanks!
I'm not 100% sure about that, but I think yes. The finally block will be invoked before the try/catch/finally block is out scoped.
ReplyDeleteThe cancel action cannot force a method out (You cannot pop a method from the stack). That is the doInBackground() method cannot be terminated by an external source until it is complete. You can check from within the doInBackground() method whether the process was cancelled or not and act accordingly.
Hello Albert,
ReplyDeleteThanks for your post. Other things work fine but when I try to do this inside the worker
portList=CommPortIdentifier.getPortIdentifiers();
I get a nasty error->
java.util.concurrent.ExecutionException: java.lang.ExceptionInInitializerError
Could you please give some pointers about what could be the problem.
Thanks.
This can be a hard error to find. If my memory serves me well, this error occurs when an exception is thrown during static initialisation. It has nothing to do with the instances, there is a problem with the static parts of a particular class.
ReplyDeleteOkay. Ya i read about that exception means some problem with static parts.
ReplyDeleteBut the funny part is it works very well when I debug the code in netbeans by putting breakpoints. It just throws this error when I do I "run project".
Additionaly when I remove that instantiation, the error goes off, so does that mean some static initialization happening inside the CommPortIdentifier class ?
Declare a variable of type CommPortIdentifier, without instantiating it and see what happens. Note that NetBeans may be providing additional resources which you're missing in the normal execution. Does this class use properties or resources (from files)?
ReplyDeleteThanks. I actually declared
ReplyDeleteCommPortIdentifier x, it ran without errors.
After seeing http://www.control.lth.se/~kurstr/InternDator/java/commapi/javadocs/javax.comm.CommPortIdentifier.html , I realize that the CommPort class has no constructor. The getPortIdentifier is a static method and needs to be called with the class reference.
So that is where the static parts are coming in maybe. Your suggestions on this?
Could you suggest how do we achieve the task of using the CommPort class in a worker. I need a serial port scanner in my worker.
Thanks.
It shouldn't be a problem whether you're using threads or not. If you want to confirm, use it with out a worker and confirm. But the exception you're getting is related with static contents. May be you're missing some property files.
ReplyDeleteI never worked with that, but if you want you can send me the code at albertattard@gmail.com and I'll have a look (later on this week - as I'm quite busy with other stuff).
Thanks a lot for that. I ll try out till then, and also send you my code. See whenever you get time.
ReplyDeleteThanks!
I figured out it has nothing to do with the worker thread certainly. Its more to do with loading a native dll and exception is coming in that.
ReplyDeletethanks. I ll try to fix it.
Regards
Hi Albert,
ReplyDeleteI have a query.
My application has a JTextArea which displays certain data when a button is clicked on the application. This JTextArea is contained in a JScrollPane. Lets say the JTextArea can accommodate 10 lines before the vertical scroll bar is invoked.
What happens is, when i click the button, the data starts coming on the JTextArea, but once the data crosses 10 lines, the scroll bar doesn't get activated until the process invoked by the button press is over. Once that process is over, the scroll bar appears. Until then the data in the text area just gets filled in the invisible region.
Lemme know if my explanation is not clear.
As an example take this text box i am typing my comment in. Imagine the scroll bar not getting activated until i finish typing the whole comment.
That's whats happening. Can i use SwingWorker to resolve this issue? If yes how?
Thanks
Akshay
From what I can understand (from your description), the task is being handled by the event thread and the scroll has to wait for your task to finish.
ReplyDeleteYou can use the SwingWorker to carry out the "long" time tasks for you. This will prevent your application from hanging.
"the scroll has to wait for your task to finish." - Correct !!!
ReplyDelete"You can use the SwingWorker to carry out the "long" time tasks for you." - Do you mean to say that i put the task that is performing the data input in the text area in the doInBackground() method? instead of putting the scroll action in it?
Hi,
ReplyDeleteYes. Thus your button should do the following:
1) Create an instance of the worker and provide all required parameters
2) Execute the worker
Extra things:
3) Disable the button programmatically
4) Enable the button when the task is finished
Hope this helps.
This is a Great Post! It's going onto my list! I've been searching for any answer and found it with your informable interface!
ReplyDeleteWhy hadn't i thought about it!
Hi Albert,
ReplyDeleteI have a problem with the publish method that accumulate all the values in a List and then call only once the process method with the whole bunch of values.
Any idea why?
Thanks for publishing this post.
Best, JCD
Hi,
DeleteI think this answers your question: http://docs.oracle.com/javase/6/docs/api/javax/swing/SwingWorker.html#publish(V...)
This is done for a good reason, as otherwise you can flood the event thread with too many requests which can easily be collapsed into one event.
Hope this helps
hi, very interesting, thank you. I tried to follow the SwingWorker in debug, and i found a curiosity that i didn't understand.. the size of the chunk list in process method is always 1.. why SwingWorker uses a list instead a single object?
ReplyDeleteHave a look at the updated version of this article at: http://www.javacreed.com/swing-worker-example/
Delete