Rich Client Developers: Avoid Java 6?

You might not see this bug on your PC. If you have ten customers, you might not see it. If you have 100…or 1000…or more…you may regret deploying your Swing application using Java 6. Because if you use a certain type of table header renderer along with the Windows XP Look and Feel, you will eventually see large numbers of exceptions.

java.lang.NullPointerException
  at com.sun.java.swing.plaf.windows.WindowsTableHeaderUI$XPDefaultRenderer.paint(
         Unknown Source)
  at javax.swing.CellRendererPane.paintComponent(Unknown Source)
  at javax.swing.plaf.basic.BasicTableHeaderUI.paintCell(Unknown Source)
  at javax.swing.plaf.basic.BasicTableHeaderUI.paint(Unknown Source)
  at javax.swing.plaf.ComponentUI.update(Unknown Source)
  at javax.swing.JComponent.paintComponent(Unknown Source)
  at javax.swing.JComponent.paint(Unknown Source)
  etc...

Here is a lesson-learned about customer error reporting. A vast majority of customers will not report errors. So we embedded an uncaught exception handler that reports all “unexpected” errors via Email. We quickly discovered that under Java 6, we see thousands of these errors. Yes, thousands.

The Worst Bug in Client Java?

This is actually a family of related problems, but they most commonly manifest themselves when using custom table header renderers.

The actual bug ID is 6429812. It only has four votes, and was originally reported under JRE 1.5. We saw it occasionally under Java 5, but it is far more common when running under Java 6.

Duplicating the Bug with Glazed Lists

Let’s start with a simple POJO:

public class Bug {
    private final int legCount;
    private final String name;

    public Bug(String name, int legCount) {
        this.name = name;
        this.legCount = legCount;
    }

    public String getName() {
        return name;
    }

    public int getLegCount() {
        return legCount;
    }
}

And here is a little Java app that shows a sortable table of bugs:

public class TableDemo extends JFrame {
  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        try {
          UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception ignored) {
          // ignored
        }
        new TableDemo().setVisible(true);
      }
    });
  }

  public TableDemo() {
    super("Table Demo");

    add(new JScrollPane(createTable()), BorderLayout.CENTER);
    pack();
    setLocationByPlatform(true);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }

  private Component createTable() {
    TableFormat<Bug> tableFormat = new BeanTableFormat<Bug>(
        Bug.class,
        new String[]{"legCount", "name"},
        new String[]{"Legs", "Name"});
    EventList<Bug> bugList = new BasicEventList<Bug>();
    bugList.addAll(Arrays.asList(new Bug("Aidan", 8), new Bug("Tanner", 6),
        new Bug("Eric", 4), new Bug("Jen", 8), new Bug("Dexter", 6)));

    SortedList<Bug> sortedBugList = new SortedList<Bug>(bugList, null);
    TableModel tableModel = new EventTableModel<Bug>(
        sortedBugList, tableFormat);
    JTable table = new JTable(tableModel);
    new TableComparatorChooser<Bug>(table, sortedBugList, false);
    return table;
  }
}

The TableComparatorChooser wraps the default table header renderer with one that provides sort arrows. This is what triggers the bug.

When you run the app, it looks like this:

Sortable Table

Duplicating the bug is easy. After starting the app, change the Windows display settings from XP to “Classic”:

  1. Right click the desktop
  2. Select Properties
  3. Change the Theme to Windows Classic

This Glazed Lists example app immediately starts spewing exceptions and no longer paints. You have to exit the app.

But but but…

The above scenario is a reliable way to reproduce the bug, but it is contrived. Most users don’t change to Classic Windows look. However we discovered many additional ways to duplicate the problem:

  • While your app is running, go to another PC and establish a Remote Desktop connection to your PC. Boom!
  • Share your desktop via WebEx. Boom!
  • Run a program like ZoomText. Boom!
  • Make the mistake of using certain screen savers. When the screen saver starts, Boom!

In summary, many programs affect the Windows graphics display mode. When these take over, your Java app will often crash.

One Partial Fix

I found a workaround. I wrote a custom table header renderer from scratch that duplicates the XP look without wrapping the original XP renderer.

We deployed this and it fixed the issue for most customers, but now we find that Java still blows up, it just happens slightly less often. Instead of blowing up in the table header renderers, it now blows up in either the BasicComboBoxUI$Handler or in WindowsTabbedPaneUI.paintRotatedSkin. Again, the same kinds of scenarios trigger these horrible bugs, although they are harder to replicate on demand.

We only know about these bugs because we deploy Swing apps to large numbers of customers and we have an automated error reporting infrastructure. Without our tools, I’m not sure we’d really know the magnitude of this problem.

Another fix is to avoid the Windows XP native look and feel.

In Summary…

These bugs are all related. There appears to be a fundamental flaw in the XP look and feel that makes rich client Java apps a highly risky proposition if your customers use XP. (which constitutes about 99% of our users, unfortunately).

I am writing about this in order to raise awareness of these highly important bugs. Leaving comments on the bug parade and voting for this bug is not enough. I cannot imagine a more important bug than one that causes the application to abort simply because someone walked away from their computer and a screen saver kicked in.

Please, Sun, fix these problems in JDK 1.6.0 update 3.


anjan bacchu Says:

hi there,

you have my vote to have this bug fixed (by sun), if that means anything.

BR,
~A

Eric Burke Says:

@anjan make sure you vote on the bug parade. :-)

jane howard Says:

Why don’t you post you article on Javalobby.com?
I’m sure much more users than those who read your blog would like to hear
about your experience.

Erik,

A quick note to say: thanks for the detailed description and analysis of your problem. I realize that this blog entry exists mostly to highlight the underlying bug in the JDK, but after some serious deliberation on our part at Glazed Lists, we’ve decided to implement a work around for the problem as best we can. Here are the gory details of our “best-effort” workaround:

The code has been changed so that TableComparatorChooser watches for changes in the UI delegate of the JTableHeader. When a change is detected, it attempts to re-wrap the NEW default renderer for the JTableHeader, if a new renderer has replaced the one TableComparatorChooser installed.

BUT, the issue is that we cannot *always* coerce the WindowsTableHeaderUI into updating the default renderer of the JTableHeader. (It turns out this code in WindowsTableHeaderUI.installUI(…) will only do it when switching TO XP mode, switching to classic mode will leave our renderer in situ):

if (XPStyle.getXP() != null) {
originalHeaderRenderer = header.getDefaultRenderer();
if (originalHeaderRenderer instanceof UIResource) {
header.setDefaultRenderer(new XPDefaultRenderer());
}
}

So, our contingency is that if the delegate renderer throws a RuntimeException, we discard it and fall back to DefaultTableCellRenderer. Blech. In this case, Table Headers won’t render like the underlying UI delegate normally would, but we feel it’s marginally better than an endless stream of NullPointerExceptions from WindowsTableHeaderUI$XPDefaultRenderer that destroys your application.

C’est la vie.