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.
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
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.
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
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:
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.
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
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.
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
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.
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.
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
Take another look at Listing 10-4. Note that a single instance of
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 |
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.