Swing Architecture

In an earlier section we saw an example of event handling in a Swing application and used an event handler -- first our own and then the handler built in to JFrame to call System.exit() to shut down the Java Virtual Machine (JVM). Without that the JVM running our HelloWorld application never exited because the application triggered activation of the Event Dispatch Thread (EDT), and the JVM will never exit while a non-daemon thread is still active.

Calling System.exit() is certainly an easy way to terminate the JVM, and in this simple application there's no reason you can't or shouldn't use it. In a real-world application, though, it's better -- and sometimes absolutely necessary -- not to use System.exit(). When you call System.exit(), you're implicitly assuming that your application is already in a state where it's appropriate for the JVM to terminate immediately: that there isn't and won't be any activity that needs to be completed before the termination occurs.

For a simple "Hello, world!" application like the one we created before that assumption isn't a problem. We know exactly what every part of the code does because the code is essentially confined to one tiny method in a single class that we wrote. In a more complex application, though, there may be separate threads -- perhaps non-daemon threads -- doing work, and calling System.exit() has the potential to terminate their work abruptly instead of allowing them to shut down gracefully and without losing any work. You can, of course, design your application code so that it "knows" about those other threads and cooperatively communicates with them or waits for them to shut down before calling System.exit(), but if you do so then you've added complexity and dependencies between parts of your code that might not need to exist. And complexity and unnecessary dependencies are what makes code difficult to enhance and debug.

So given that using System.exit() is usually a bad idea, what's the alternative? We still need a way to have our HelloWorld application terminate when the user closes its frame, but as we saw before, the JVM will only terminate when System.exit() is called or when there are no more non-daemon threads. If we aren't supposed to use System.exit(), then obviously we need some way to eliminate the non-daemon threads, but Swing doesn't provide us with a way to interact with or terminate the EDT -- at least not directly.

In fact, there is a condition that will cause the EDT to terminate on its own, which is hinted at in the Application Programming Interface (API) documentation for WindowConstants.DISPOSE_ON_CLOSE. That documentation states that, "When the last displayable window within the Java virtual machine (VM) is disposed of, the VM may terminate" and it links to an AWT Threading Issues page on Oracle's web site. The information on that page is somewhat outdated in terms of the terminology used but is still mostly relevant and buried within the page is this key piece of information:

Therefore, a stand-alone AWT application that wishes to exit cleanly without calling System.exit must:
  • Make sure that all AWT or Swing components are made undisplayable when the application finishes. This can be done by calling Window.dispose on all top-level windows.
It also includes a second bullet item specifying that you should be sure your event listeners can't enter an infinite loop, but the first item is what's relevant here, and even though it specifically refers to an "AWT application", the point is equally applicable to Swing. In other words, what it says is that the EDT will exit on its own when all top-level windows are disposed. We'll see later what a "top-level" window is, but for the time being it's sufficient to know that the frame created by our "Hello, world!" application is one. And if we dispose it, then based on this information the JVM should terminate, which is exactly what does happen if you make the change shown in bold in Listing 1:

import javax.swing.JFrame; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; public class CloseableHelloWorld { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frame.setTitle("Hello, world!"); frame.setSize(400, 225); frame.setVisible(true); }); } }
Listing 1: Disposing a frame when the user requests that it be closed.

Using DISPOSE_ON_CLOSE is just a way to have the frame's built-in window listener call the frame's dispose() method that it inherits from the Window class, and once again the API documentation proves to be a useful reference. In the case of the dispose() documentation, it states that the method does the following:

Releases all of the native screen resources used by this Window, its subcomponents, and all of its owned children. That is, the resources for these Components will be destroyed, any memory they consume will be returned to the OS, and they will be marked as undisplayable.
For anyone new to Swing this explanation probably isn't helpful because it uses a number of terms that may not be understood. By the end of this tutorial (though not this current section) we'll have covered all of the concepts referenced in this quote, but for now let's concentrate on the first and arguably most confusing: what exactly is a "native screen resource"? To understand that question it's helpful to provide a brief review of Swing's history, particularly why it was created in the first place and what considerations were considered key during its design.

Abstract Window Toolkit (AWT) Design

When Java was first introduced in 1995, web applications were primitive and relatively uncommon. Desktop applications still very much dominated the software industry, and including an API for developing them was seen as critical to Java's success. The original attempt at accomplishing this was the Abstract Window Toolkit (AWT), which was included in Java 1.0 and is technically still supported today.

AWT's design philosphy is simple: it's a Java API that's essentially just a "wrapper" around the widgets (windows, buttons, etc.) provided by the operating system (OS), whether that operating system is Windows, Linux, Mac OS, etc. The beauty of this approach was that it was relatively easy to implement because all of the really complex work is handled by the operating system itself, and the methods defined in the AWT classes mostly just pass through requests to the native operating system. For example, if you create an instance of java.awt.Button, the Java code simply requests that the native operating system create one of its buttons, and the Java Button object maintains a handle (reference) to its native operating system counterpart, commonly called its peer. Similarly, if you call the button's setLabel(), the Java code just calls an equivalent operating system method for its peer, passing through the text you specified that should appear as the button's label.

Because of this design, AWT components are referred to as heavyweight because they're relatively "heavy" or "expensive" in terms of memory and processing time to construct and maintain: when you instantiate and use one, the Java code creates a Java object and also invokes native code that creates and returns a reference to an appropriate native peer. Java does attempt to minimize the cost of heavyweight widgets by not creating a peer until the widget is actually going to be displayed. So, for example, when you create a new Button instance, it initially has no native peer but one will be created and assigned automatically if the button is displayed. This on-demand approach is known in programming circles as lazy initialization and is a commonly used to handle the creation of expensive resources.

Swing's Lightweight Design

One of the Java's most appealing characteristics has always been it's cross-platform compatibility: that is, the ability for you to create a Java program on Windows and have it function exactly the same way on any operating system where Java is supported. Early on that capability was described as, "Write Once, Run Anywhere" (WORA), and was often used to promote Java in its early days, but the promise didn't always live up to the reality, especially when it came to AWT applications. Although most operating system platforms do have a lot in common with respect to their user interface widgets, there are differences between platforms and sometimes those differences were enough to cause problems with AWT application developed on one OS and executed on a different one. For Java to be successful it needed an API that supported a truly cross-platform solution for developing desktop applications.

The solution that was chosen was to simply create a set of widgets that are written completely (or almost completely) using Java code. That code could be made part of the core Java classes, would be available on every platform to which Java was implemented, and the widgets would look and function exactly the same way on every platform. That solution, ultimately named "Swing", was included in Java 1.2 and over the next few major releases evolved into a very robust and reliable platform on which to build desktop applications.

In contrast to AWT's heavyweight widgets, Swing classes and their instances are described as being lightweight, because they're relatively inexpensive in terms of the amount of memory and processor time needed to support them. Although Swing is sometimes described as an alternative to the AWT, it's more accurately to say that it's an extension of AWT or to say that it's built on the AWT, because it's not possible to create a Swing application without at least indirectly using AWT classes. In fact, as mentioned in a previous section, all classes in both AWT and Swing that represent visual user interface widgets (frames, buttons, etc.) inherit from the AWT's Component class, which is why they're collectively and generically referred to as "components".

To summarize, AWT components are heavyweight / have native peers while Swing components generally are lightweight / do not. We say "generally" because there are some important exceptions where a class defined in the javax.swing package represents a heavyweight component. Specifically, the Swing types that inherit from AWT's Window class are heavyweight, namely JWindow, JDialog, and the JFrame class we used for our "Hello, world!" application. This is why the windows you see on your system can vary from the ones shown in this tutorial: because the frame displayed is a native peer that represents what your system has determined a frame should look like. For example, Figure 2 shows an example of what our "Hello, world!" program's user interface can look like when run on a Linux operating system. In particular, note that although it supports minimize, maximize, and close buttons that there's no popup menu as there is on Windows.
'Hello, world!' application window
Figure 2: The "Hello, world!" application on Linux.

Knowing that JFrame is heavyweight despite being a Swing component, let's return to our goal of getting the EDT to terminate itself and clarify why disposing of the frame causes the EDT to exit. Remember that Oracle's documentation states that, "a stand-alone AWT application that wishes to exit cleanly without calling System.exit() [must] make sure that all AWT or Swing components are made undisplayable." In this context, "undisplayable" is just another term for not having a peer:

"Undisplayable" is an appropriate term for not having a peer because when a widget does appear on the screen, it's the peer that you see -- not the Java object.
the documentation is essentially saying that the EDT will remain active as long as there are components with native peers, and as we just established, a displayed JFrame has a peer. However, the documentation also mentioned that the peer of a window can be eliminated by calling dispose(), which is why the EDT terminated -- followed by the entire JVM process -- after our frame was disposed. This sequence is illustrated below:

  1. The JVM process is activated and creates the main thread that executes our main() method.
  2. An instance of JFrame is created.
  3. The setVisible(true) invocation causes a peer to be created for the frame and the EDT to be activated.
  4. The non-daemon thread created by the JVM exits the main() method, but the EDT remains alive, keeping the JVM from exiting.
  5. When the user clicks on the frame's Close button or activates the equivalent system menu item, the frame is disposed, destroying its native peer and making it disappear.
  6. The EDT determines that no more native peer resources exist so it stops executing.
  7. The JVM determines that no more non-daemon threads are alive so it terminates itself.

It's appropriate for the EDT to terminate when no native peer windows exist because its primary purpose is to provide rendering and processing of input that occurs within those windows. When no more usable windows exist, the EDT can and should terminate because it's no longer needed.

Note that disposing a component / destroying its peer isn't an irreversible operation. If you dispose a component such as a frame, you can make it visible again after that by calling setVisible(true), at which time the underlying Java code will quietly create a new peer for it. Likewise, the activation of the EDT can technically occur more than once, but this is less common because -- as in this case -- the existance of the EDT usually coincides with the life cycle of the application: it's started when the application is initialized and terminated when it's time for the application to exit.

One question that may have occurred to you is how Swing components can be visible if only heavyweight components can appear on the screen. In reality, it's more accurate to say that the operating system only ever knows about and renders heavyweight components. The rendering of Swing components by the EDT is translated into drawing done on the window by the native OS, so to the native operating system it's just an application drawing in an arbitrary way onto its window. For example, if you create a frame that displays a Swing button, the Swing code renders onto the window a button representation, though again the native operating system is oblivious to the idea that a button is supposed to exist. Similarly, if you then click on the button the native operating system will detect that a mouse click occurred over the window and will pass an event to the EDT describing the input that occurred. When the EDT processes that event, however, the Java code will determine that the click took place over the area occupied by a button it drew and will handle the event appropriately.