[Contents] [Prev] [Next] [Index]

CHAPTER 10 - Swing Models and Renderers

The Java Foundation Classes (JFC) Swing toolkit (Swing) provides a comprehensive set of classes for creating highly interactive graphical user interfaces with the Java platform. Swing is highly flexible, but also quite complex. While novice programmers can successfully use Swing to create basic graphical user interfaces (GUIs), to create a truly complex, professional-quality GUI you must understand Swing's architectural underpinnings. This is especially true when using Swing's more complex, renderer-based components such as JTable, JTree, JComboBox, and JList. The control provided by Swing's models and renderers is critical to writing high-performance, scalable GUIs.

This chapter provides an overview of Swing's component architecture, and explains how renderer objects extend this architecture to support components that display large datasets. Section 10.2 presents some tactics for improving performance by writing your code with knowledge of Swing's models in mind. Section 10.2.3 walks through a sample spreadsheet application that uses custom models and renderers to improve performance and reduce footprint.

10.1 Swing's Component Architecture

The original UI toolkit for the Smalltalk system used a pattern known as Model- View-Controller (MVC). MVC introduced the concept that a data source should be isolated from its onscreen representation. This is a powerful architecture that enables improved code reuse and program architectures. Swing uses a modified version of the MVC architecture, shown in Figure 10-1.
Swing component architecture

Typical Swing GUI components consist of at least three objects: a Component, a Model, and a UI Delegate. In Swing's architecture, the Model is charged with storing the data, while the UI Delegate is responsible for getting data from the Model and rendering it to the screen. The Component generally coordinates the actions of the Model and Delegate, while also acting as glue to the AWT windowing system.

Note that the UI Delegate can be replaced at runtime. This enables Swing's pluggable look-and-feel (PLAF) system, illustrated in Figure 10-2.

While Swing's modified MVC architecture clearly provides benefits in terms of flexibility, it has often been fingered as the cause of poor performance for some applications. Although it is true that an MVC-based architecture uses more method invocations to support the extra level of indirection, this cost is minimal.

Profiling Swing-based applications shows that the overhead for model-view separation is literally lost in the noise-it's less than one percent of CPU consumption. (The bulk of processing time for a complex Swing-based user interface is actually spent on low-level graphics operations.) Rather than being a cause of poor performance, Swing's model-view architecture is critical for building scalable programs.

Swing's PLAF system

10.2 Scalable Components

Swing provides a number of GUI components that are designed to manipulate datasets that are potentially very large, including

These scalable components are designed to be able to manipulate thousands, or even millions of pieces of data. To do this without using massive amounts of memory, these components add the concept of a renderer to Swing's architecture. Figure 10-3 shows the modified architecture.

10.2.1 Renderers

In these more complex Swing components, the renderer is key for providing scalability. Let's look at JTable as an example of why renderers exist. A naively implemented table might use a JLabel for each cell in the table. While this might work for small datasets, it doesn't work for large ones. For example, if you tried to display a 1,000 row by 1,000 column spreadsheet with such a table, the RAM requirements might be close to a gigabyte, even if every cell is empty.
Scalable component architecture

To get around this scalability problem, Swing's JTable uses a single component to paint all of the cells that contain a particular data type. For example, all cells that contain String objects are drawn by the same component. This type of component is called a renderer. Using a renderer to display multiple cells drastically reduces the storage requirements for a large table.

When renderers are used to display a table, the data for a cell is fetched from the model, a renderer is configured based on that data, and then the cell is painted. The renderer is then moved to the location of the next cell and the process is repeated. The procedure is shown in Figure 10-4.

It's important to note that you can control this process by manipulating the renderers and models. All of the scalable components, such as JTree and JList, use this renderer approach; it isn't limited to JTable.

Rendering a component

10.2.2 Models

The ability to manipulate Swing models directly is critical for writing scalable user interfaces. As a simple example, take a look at Listing 10-1.

JComboBox box = new JComboBox();
for (int i = 0; i < numItems; i++) {
    box.addItem(new Integer(i));
}
Adding items to a JComboBox

This code simply adds a number of items to a JComboBox. The code is similar to the code you would use to load items into an AWT Choice box. This approach works fine for small numbers of items, but its inefficiency becomes apparent when a large number of items are added.

Although Listing 10-1 does not explicitly reference any models, the JComboBox object's model is involved. Each time you call addItem on the JComboBox, a fairly substantial amount of work is done-the component passes the request to the JComboBox model and the model posts an event to indicate that an item has been added. It turns out that you can accomplish the same thing much more efficiently by directly accessing the model, as shown in Listing 10-2.

Vector v = new Vector(numItems);
for (int i = 0; i < numItems; i++) {
   v.add(new Integer(i));
}
ComboBoxModel model = new DefaultComboBoxModel(v);
JComboBox box = new JComboBox(model);
Faster JComboBox loading

Why is this faster? The reason is twofold. First, because all the items are added to the model at once instead of one by one, only one event needs to be posted. This means that fewer event objects are created and fewer methods are called. Second, because fewer objects need to be notified of changes, less work is required. In general, the amount of work done equals the number of notifications multiplied by the number of listeners. Since the model is newly created, the number of listeners is zero, which means that no notifications are posted.

Combo box load times

There are two lessons to be learned here:


So, how much faster is it to avoid the extra notifications? Figure 10-5 shows the results of using the examples in Listing 10-1 and Listing 10-2 to load different numbers of items.

As you can see, the first option scales very poorly. In fact, it takes more than two seconds to load 5,000 items into the JComboBox using the first approach. Loading the model in bulk takes a mere 50 milliseconds.

The number of notifications can have a large effect on your program's start-up time. It can also affect the amount of time it takes to open dialog boxes and perform similar operations.

10.2.3 Example: Simple Spreadsheet

A simple spreadsheet example, SheetMetal, is used in the following sections to illustrate several optimizations that can be made through creative use of renderers and models. The SheetMetal UI is shown in Figure 10-6.

Many of the optimizations described are application-specific. Some result in small improvements in performance, and others result in significant improvements. These optimizations illustrate the types of performance improvements you can make. They aren't meant to imply that you should make these specific optimizations in your own code; instead, they're designed to help you understand Swing's component architecture and make your own custom optimizations.

SheetMetal spreadsheet application

10.2.4 Using Custom Models

Swing contains a number of default models, such as DefaultTableModel. These models are generic and are suited to a wide variety of light-duty uses. For example, the DefaultTableModel is implemented internally as a Vector of Vector objects. It is quite usable for small data sets. However, it is clearly not the fastest or most space-efficient data structure. In general, when dealing with complex datasets, you should create your own custom model. The Swing model-view architecture was designed to give you this flexibility.

Let's look at the model requirements for the SheetMetal application. Spreadsheets are often used to hold very large datasets. For this example, we'll assume SheetMetal must be able to handle a dataset that contains 1,000 rows and 1,000 columns. If this functionality were implemented with the DefaultTableModel, the RAM requirements would be huge. DefaultTableModel is implemented using Vector, and Vector is implemented in terms of an array. On a 32-bit system, each array element is likely to use 4 bytes. That means the RAM requirements for this data structure are approximately 1,000 x 1,000 x 4, or 4 million bytes-even before any data is stored in the table!

When you analyze typical complex spreadsheets, you find that they rarely contain data in all cells. A complex spreadsheet is usually sparsely populated, with blocks of data here and there and blank cells in between. An array-based data structure, such as DefaultTableModel, consumes space even for empty cells. A custom, sparse model would be more appropriate for our spreadsheet program. Listing 10-3 shows a model implemented for SheetMetal using a HashMap as the underlying storage mechanism.

public class SpreadsheetModel extends AbstractTableModel {
   public static int DEFAULT_ROW_COUNT = 1024;
   public static int DEFAULT_COLUMN_COUNT = 1024;

   private Map sparseMatrix = new HashMap();
   private int maxRow = 0;
   private int maxColumn = 0;

   private Point tmpIndex = new Point(0,0);

   public int getRowCount() {
      return DEFAULT_ROW_COUNT;
   }

   public int getColumnCount() {
      return DEFAULT_COLUMN_COUNT;
   }
   public Object getValueAt(int row, int column) {

      tmpIndex.y = row;
      tmpIndex.x = column;
      Object returnVal = sparseMatrix.get(tmpIndex);
      if (returnVal != null) {
         return returnVal;
      } else {
         return "";
      }
   }

   public void setValueAt(Object val, int row, int column) {
      if (val == null) {
         sparseMatrix.remove(new Point(column, row));
         return;
      }
      maxRow = Math.max(row, maxRow);
      maxColumn = Math.max(column, maxColumn);
      sparseMatrix.put(new Point(column, row), val);
   }

   public boolean isCellEditable( int row, int column ) {
      return true;
   }

   public int getMaxRow() {
      return maxRow;
   }

   public int getMaxColumn() {
      return maxColumn;
   }
}
A sparse TableModel

The advantage of this custom data structure is that it starts out very small and grows as more data is added. Where the array-based structure uses several megabytes of memory when it's created, this one uses only about a kilobyte.

Although this structure is somewhat slower than an array-based structure, profiling shows that the overhead is only about 1 percent of the overall time it takes to render the table.

Obviously, it wouldn't make sense to always use a sparse model like this one. If the data is truly dense, the storage requirements for this type of structure are far greater than for the array-based structure. The point is to keep in mind that you have complete control over how your data is stored. Using a custom model can result in a large savings-and using the wrong model can carry a hefty penalty. This tactic isn't limited to JTable; you can use custom models with other controls as well.


10.2.5 Using Custom Renderers

Custom renderers are commonly created to control the appearance of a JTable, JTree, or JList. However, custom renderers can also sometimes be used to improve performance. Conversely, when implemented poorly, custom renderers can be highly detrimental to your program's performance. It's crucial that you understand the renderer mechanism so you can make the right implementation decisions.

As shown in Figure 10-4, each time a cell in a JTable is drawn, the data is fetched from the model and used to configure the renderer. The renderer is then used to draw the contents of the cell. The previous section mentions that it is common for spreadsheet data to be sparse-many cells are totally empty. Although the DefaultTableCellRenderer is smart enough not to draw anything for empty cells, there is still a lot of configuration and setup overhead for each cell. Since we know that the data for the SheetMetal application is sparse, we can write a custom renderer that is optimized for sparse data. This renderer is shown in Listing 10-4.

public class FastStringRenderer extends DefaultTableCellRenderer {

   Component stubRenderer = new NothingComponent();

   public Component getTableCellRendererComponent(JTable table,
                                                  Object value,
                                                  boolean isSelected,
                                                  boolean hasFocus,
                                                  int row, 
                                                  int column) {

      if ( ((String)value).length() == 0 && 
           !isSelected && !has-Focus) {
         return stubRenderer;
      }

      return super.getTableCellRendererComponent(table, value,
                                                 isSelected,
                                                 hasFocus,
                                                 row, column);
   }

   class NothingComponent extends JComponent {
      public void paint(Graphics g) {
         // Do Nothing
      }
   }
} 
Sparse data renderer

getValueAt

When creating your own models, keep in mind that the getValueAt method is called every time a cell is rendered. Neither the Component or Renderer cache the value; they request the data each time a cell needs to be updated. Since getValueAt is called so often, it has the potential to become a major bottleneck for your program.

The Swing team once received a piece of code from a developer who was trying to figure out why his application was performing so poorly. It turned out that the developer's custom model performed complex matrix arithmetic inside its getValueAt method. This matrix computation calculated the state of the entire table, not just a single cell. Once the developer understood the model architecture, he was able to restructure his code to run much faster.

This sparse data renderer short-circuits the normal rendering process in two ways. First, it checks to see if the cell is empty (the string length is zero). Second, it checks to make sure that the cell isn't selected and doesn't have the focus. If the cell doesn't meet these conditions, the cell needs to be drawn and should be processed normally. To process the cell normally, the inherited version of getTableCellRendererComponent is called. This implementation sets up the component and returns the cell settings, such as its colors, borders, and other properties.

If the cell meets all of the conditions-the cell is totally empty, it isn't selected, and doesn't have the focus-all of the setup operations are bypassed and a NothingComponent is returned. Avoiding the setup costs eliminates numerous unnecessary method invocations.

The other optimization implemented in the fast renderer is that NothingComponent overrides the paint method so it doesn't do anything. Although the DefaultTableCellRenderer is smart enough to detect that the cell is empty and not draw anything, it still performs a considerable amount of setup and computation. By totally canceling out the call to paint, this unnecessary work is avoided.

Table 10-1 shows a spreadsheet's scrolling speed with the default renderer and with the sparse data renderer. Measurements for both renderers are shown with different amounts of data in the spreadsheet: empty, sparsely populated, and densely populated. The time is the number of milliseconds it took to scroll through 200 rows of the table.

Spreadsheet Scrolling Speed

Table State


Default Renderer


Sparse Data Renderer


Empty


1,150 ms


990 ms


1/3 full


1,150 ms


1,050 ms


Full


1,150 ms


1,200 ms

As you can see, the results of this optimization are noticeable, but not dramatic. For a table of empty cells, the sparse data renderer is about 15 percent faster. For a sparsely populated table (1/3 full), the fast renderer is about 10 percent faster. Notice, however, that when all of the cells in the table are full, the sparse data renderer is actually about 4 percent slower.

While this particular optimization isn't necessarily appropriate in all cases, it demonstrates that controlling the rendering process by implementing a custom renderer can be a worthwhile optimization. In your own programs, this tactic might open up opportunities for caching, short-circuiting, or other aggressive optimizations.

Rendering Component Objects

While custom renderers can improve performance, they can be a major problem if implemented incorrectly. One common mistake that we've seen several developers make can lead to poor performance or even cause a program to terminate with an OutOfMemoryError.

One of the main reasons that the renderer subsystem exists is that it would require too many resources to represent each table cell with a separate Component. This means that a getTableCellRendererComponent method should not create a new Component each time it is called.

Take another look at Listing 10-4. Note that a single instance of NothingComponent is created when the renderer is initialized. This instance is returned each time one is needed. DefaultTableCellRenderer actually inherits from JLabel and typically returns a reference to this each time a Component is needed for rendering.

The key is to reuse the same instance each time, changing only the configuration information each time you return it. Otherwise, you're likely to create thousands of Component objects, which can quickly swamp the garbage collector with megabytes of temporary objects.


10.2.6 Using Custom Models and Renderers Together

You don't need to choose between using a custom model and a custom renderer- both can be used in the same JComponent. The row header in the SheetMetal application provides an example of this. Swing provides an easy-to-use mechanism to label table columns, but doesn't provide one for rows. Fortunately, it is fairly easy to create one.

The simplest way to label rows is to create a new table to act as the row header. This JTable customizes both the model and the renderer. Listing 10-5 shows the SpreadsheetRowHeader used in SheetMetal. SpreadsheetRowHeader contains two inner classes: RowHeaderRenderer and RowHeaderModel.

public class SpreadsheetRowHeader extends JTable {
   TableCellRenderer render = new RowHeaderRenderer();

   public SpreadsheetRowHeader(JTable table) {
      super(new RowHeaderModel(table));
      configure(table);

   }
   protected void configure(JTable table) {
      setRowHeight(table.getRowHeight());
      setIntercellSpacing(new Dimension(0,0));
      setShowHorizontalLines(false);
      setShowVerticalLines(false);
   }

   public Dimension getPreferredScrollableViewportSize() {
      return new Dimension(32, super.getPreferredSize().height);
   }
   public TableCellRenderer getDefaultRenderer(Class c) {
      return render;
   }

   static class RowHeaderModel extends AbstractTableModel {
      JTable table;
      protected RowHeaderModel(JTable tableToMirror) {
         table = tableToMirror;
      }
      public int getRowCount() {
         return table.getModel().getRowCount();
      }
      public int getColumnCount() {
         return 1;
      }
      public Object getValueAt(int row, int column) {
         return String.valueOf(row+1);
      }
   }
   static class RowHeaderRenderer extends DefaultTableCellRenderer {
     public Component getTableCellRendererComponent(JTable table, 
                                                     Object value,
                                                     boolean isSelect,
                                                     boolean hasFocus,
                                                     int row,
                                                     int column) {
   
         setBackground(UIManager.getColor("TableHeader.background"));
         setForeground(UIManager.getColor("TableHeader.foreground"));
         setBorder(UIManager.getBorder("TableHeader.cellBorder"));
         setFont(UIManager.getFont("TableHeader.font"));
         setValue(value);
         return this;
      }
   }
}
Table row header

RowHeaderRenderer is in charge of actually drawing the cells of the row header. From a performance perspective, there's nothing particularly interesting about the RowHeaderRenderer class. It simply configures itself to look like Swing's built-in column headers by accessing values from the UIManager.

RowHeaderModel controls how the table data is stored. From a performance perspective, the RowHeaderModel class is quite interesting. Since it's possible to have literally millions of rows in a JTable, it would be nice if the row header's storage requirements were not bound to the number of rows in the table. This is exactly how RowHeaderModel works. As a result, it uses virtually no storage and its RAM requirements don't grow with the number of rows in the table. The only storage allocated by RowHeaderModel is a reference to the JTable that it is labeling. The RowHeaderModel getValueAt method simply converts the row number passed to the method into a String-no long-term storage is allocated.

Key Points

  • Swing's model-renderer architecture allows you to control how your data is stored and displayed. This is key to creating components that manipulate very large datasets.
  • When changing data stored in models, perform the operations in bulk whenever possible. This reduces the number of events posted by the model.
  • Use custom models to handle large datasets. The default models provided with Swing are generic and designed for light-duty use.
  • Custom renderers can sometimes be used to improve performance.
  • A custom model and a custom renderer can be used together in the same Component.




[Contents] [Prev] [Next] [Index]

Copyright © 2001, Sun Microsystems,Inc.. All rights reserved.