Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Java Tutor is my platform for teaching about Java 7+ and JavaFX 2.0+, mainly via programming projects.
Swing is a single-threaded UI toolkit that uses the EDT to update and send input events to components. Because you must be careful in how you interact with this thread to ensure that your Swing applications work correctly, this tutorial presents and explains two rules that you need to follow to avoid potential problems.
HelloSwing application source code.
// HelloSwing.java
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class HelloSwing
{
public static void main(String[] args)
{
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane (new JLabel("Hello, Swing", JLabel.CENTER));
frame.pack();
frame.setVisible(true);
}
}
Listing 1: HelloSwing.java (version 1)
main(String[]) first instantiates the javax.swing.JFrame class to create a frame window in which to
house the UI. It then invokes this class's setDefaultCloseOperation(int) method with the EXIT_ON_CLOSE constant to tell Swing to terminate the application when the user clicks the x button on the menubar.
main(String[]) next instantiates javax.swing.JLabel to create a Swing label component with some text -- JLabel.CENTER informs this component to center this text. The label component is then passed to JFrame's setContentPane(Container) method to establish the label and the frame window's content.
Finally, main(String[]) calls JFrame's pack() method to pack the frame window's content to its preferred size, which is just large enough to display the label. It also calls JFrame's setVisible(boolean) method with a true argument to realize (make visible) the frame window and its label content.
This code looks okay and appears to work properly, so why is it incorrect? Because Swing is single-threaded, most Swing objects (such as JFrame objects) are not thread-safe. Accessing these objects from multiple threads risks thread interference and/or memory inconsistency errors:
counter variable while another thread updates that variable. Because a long integer being read/written on a 32-bit machine requires two read/write accesses, it is possible that the reading thread reads part of this variable's current value, the writing thread updates the variable, and then the reading thread reads the rest of the variable. The result is that the reading thread has an incorrect value.
counter and then the reading thread reads this variable. However, it doesn't obtain the value that was written because each thread has its own local copy of the variable (through a caching mechanism being used to boost performance).
How might these problems occur when the UI is not created on the EDT (as demonstrated in Listing 1)? John Zukowski demonstrates one scenario in his JavaWorld article titled Swing threading and the event-dispatch thread.
Zukowski presents an example that adds a container listener to a frame window container component. Listener methods are called when a component is added to or removed from the frame. He demonstrates the EDT running code within a listener method before the frame window is realized on the main thread.
After the EDT starts to run in a listener method, and while the main thread continues to initialize the UI, components could be created by the main thread and accessed by the EDT. The EDT might attempt to access these components before they exist, and doing so could crash the application.
Even if the main thread creates the components before the EDT accesses them from the listener method, the EDT may have an inconsistent view (because of caching), and is unable to access the references to the new components. A program crash (probably a thrown NullPointerException instance) would most likely occur.
You can prevent these and other problems by offloading UI creation to the EDT. Accomplish this task by placing the UI creation code in a runnable (an object whose class implements the java.lang.Runnable interface) and passing this runnable to the javax.swing.SwingUtilities or java.awt.EventQueue class's invokeLater(Runnable) method, as demonstrated in Listing 2.
// HelloSwing.java
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class HelloSwing
{
public static void main(String[] args)
{
Runnable r = new Runnable()
{
public void run()
{
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane (new JLabel("Hello, Swing",
JLabel.CENTER));
frame.pack();
frame.setVisible(true);
}
};
EventQueue.invokeLater(r);
}
}
Listing 2: HelloSwing.java (version 2)
HelloSwing's main(String[]) method first creates a runnable, by instantiating an anonymous class that implements Runnable. It then passes this instance to EventQueue's invokeLater(Runnable) method. At some future point, Swing's EDT will invoke the runnable's run()
method to create the UI.
Note: The SwingUtilities class's invokeLater(Runnable) method is a wrapper that calls the same-named method in the EventQueue class. Zukowski presents the reason for this architecture in the aforementioned article.
|
ViewPage application source code.
// ViewPage.java
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public class ViewPage
{
public static void main(String[] args)
{
Runnable r;
r = new Runnable()
{
public void run()
{
final JFrame frame = new JFrame("View Page");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.add(new JLabel("Enter URL"));
final JTextField txtURL = new JTextField(40);
panel.add(txtURL);
frame.getContentPane().add(panel, BorderLayout.NORTH);
final JTextArea txtHTML = new JTextArea(10, 40);
frame.getContentPane().add(new JScrollPane (txtHTML),
BorderLayout.CENTER);
ActionListener al;
al = new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
InputStream is = null;
try
{
URL url = new URL(txtURL.getText());
is = url.openStream();
StringBuilder sb = new StringBuilder();
int b;
while ((b = is.read()) != -1)
sb.append((char) b);
txtHTML.setText(sb.toString());
}
catch (IOException ioe)
{
txtHTML.setText(ioe.getMessage());
}
finally
{
txtHTML.setCaretPosition(0);
if (is != null)
try
{
is.close();
}
catch (IOException ioe)
{
}
}
}
};
txtURL.addActionListener(al);
frame.pack();
frame.setVisible(true);
}
};
EventQueue.invokeLater(r);
}
}
Listing 3: ViewPage.java (version 1)
main(String[]) creates a user interface consisting of a textfield for entering an HTML document's URL, and a scrollable textarea for displaying the document's HTML. Pressing the Enter key after entering the URL causes ViewPage to fetch the HTML document and display the results.
Listing 3 is problematic because the document is read on the EDT. The UI becomes unresponsive when it takes a long time to read the document (because of a slow network, for example). The solution to this problem involves using a worker thread (a thread that runs in the background) to read the document. Listing 4 presents one version of this solution.
// ViewPage.java
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.InputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public class ViewPage
{
public static void main(String[] args)
{
Runnable r;
r = new Runnable()
{
public void run()
{
final JFrame frame = new JFrame("View Page");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.add(new JLabel("Enter URL"));
final JTextField txtURL = new JTextField(40);
panel.add(txtURL);
frame.getContentPane().add(panel, BorderLayout.NORTH);
final JTextArea txtHTML = new JTextArea(10, 40);
frame.getContentPane().add(new JScrollPane (txtHTML),
BorderLayout.CENTER);
ActionListener al;
al = new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
txtURL.setEnabled(false);
Runnable worker = new Runnable()
{
public void run()
{
InputStream is = null;
try
{
URL url = new URL(txtURL.getText());
is = url.openStream();
final StringBuilder sb;
sb = new StringBuilder();
int b;
while ((b = is.read()) != -1)
sb.append((char) b);
Runnable r = new Runnable()
{
public void run()
{
txtHTML.setText(sb.toString());
txtURL.setEnabled(true);
}
};
try
{
EventQueue.invokeAndWait(r);
}
catch (InterruptedException ie)
{
}
catch (InvocationTargetException ite)
{
}
}
catch (final IOException ioe)
{
Runnable r = new Runnable()
{
public void run()
{
txtHTML.setText(ioe.getMessage());
txtURL.setEnabled(true);
}
};
try
{
EventQueue.invokeAndWait(r);
}
catch (InterruptedException ie)
{
}
catch (InvocationTargetException ite)
{
}
}
finally
{
Runnable r = new Runnable()
{
public void run()
{
txtHTML.setCaretPosition(0);
txtURL.setEnabled(true);
}
};
try
{
EventQueue.invokeAndWait(r);
}
catch (InterruptedException ie)
{
}
catch (InvocationTargetException ite)
{
}
if (is != null)
try
{
is.close();
}
catch (IOException ioe)
{
}
}
}
};
new Thread(worker).start();
}
};
txtURL.addActionListener(al);
frame.pack();
frame.setVisible(true);
}
};
EventQueue.invokeLater(r);
}
}
Listing 4: ViewPage.java (version 2)
Listing 4 differs from Listing 3 in that it creates a worker thread to read the HTML document via a runnable. After this document is read, the invokeAndWait(Runnable) method is called to update the UI on the EDT via another runnable. The result is a responsive UI because the EDT is never delayed.
Caution: Never call invokeAndWait(Runnable) from the EDT. Doing so blocks the EDT, which is required to execute the runnable.
|
Although Listing 4 solves the unresponsive UI problem, the solution is somewhat verbose and non-intuitive. For example, you might wonder why sb isn't declared volatile. This isn't necessary because sb is final, and the construction of its containing class creates a happens-before relationship with any
subsequent reads.
Listing 5 presents a more intuitive solution that leverages the abstract javax.swing.SwingWorker class (new in Java SE 6).
// ViewPage.java
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingWorker;
public class ViewPage
{
public static void main(String[] args)
{
Runnable r;
r = new Runnable()
{
public void run()
{
final JFrame frame = new JFrame("View Page");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.add(new JLabel("Enter URL"));
final JTextField txtURL = new JTextField(40);
panel.add(txtURL);
frame.getContentPane().add(panel, BorderLayout.NORTH);
final JTextArea txtHTML = new JTextArea(10, 40);
frame.getContentPane().add(new JScrollPane (txtHTML),
BorderLayout.CENTER);
ActionListener al;
al = new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
txtURL.setEnabled(false);
class GetHTML extends SwingWorker<StringBuilder,
Void>
{
private String url;
GetHTML(String url)
{
this.url = url;
}
@Override
public StringBuilder doInBackground()
{
StringBuilder sb = new StringBuilder();
InputStream is = null;
try
{
URL url = new URL(this.url);
is = url.openStream();
int b;
while ((b = is.read()) != -1)
sb.append((char) b);
return sb;
}
catch (IOException ioe)
{
sb.setLength(0);
sb.append(ioe.getMessage());
return sb;
}
finally
{
if (is != null)
try
{
is.close();
}
catch (IOException ioe)
{
}
}
}
@Override
public void done()
{
try
{
StringBuilder sb = get();
txtHTML.setText(sb.toString());
txtHTML.setCaretPosition(0);
}
catch (ExecutionException ee)
{
txtHTML.setText(ee.getMessage());
}
catch (InterruptedException ie)
{
txtHTML.setText("Interrupted");
}
txtURL.setEnabled(true);
}
}
new GetHTML(txtURL.getText()).execute();
}
};
txtURL.addActionListener(al);
frame.pack();
frame.setVisible(true);
}
};
EventQueue.invokeLater(r);
}
}
Listing 5: ViewPage.java (version 3)
This final version of ViewPage relies on GetHTML, a SwingWorker subclass that's declared in the actionPerformed(ActionEvent) method, to read the HTML document on a worker thread (keeping the user interface responsive), and update the user interface with the HTML on the EDT (where Swing code must execute).
When actionPerformed(ActionEvent) is called (the user presses Enter after entering a URL in the textfield), this method instantiates GetHTML with the textfield's text (the textfield is not accessed from the worker thread because Swing is single-threaded), and calls SwingWorker's execute() method.
execute() causes GetHTML's overriding doInBackground() method to be called on a worker thread, which populates a java.lang.StringBuilder object with HTML/error text and returns this object. The EDT then calls the overriding done() method, which accesses the StringBuilder object by calling SwingWorker's get() method, and populates the textarea with these contents.
Note: SwingWorker's get() method throws an instance of the java.util.concurrent.ExecutionException class if an exception is thrown while attempting to retrieve the object returned from doInBackground().
|
You can download this post's code and answers here. Code was developed and tested with JDK 7u2 on a Windows XP SP3 platform.
* * *
I welcome your input to this blog, and will write about relevant topics that you suggest. While waiting for the next blog post, check out my TutorTutor website to learn more about Java and other computer technologies (and that's just the beginning).