Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-6830678

JTable throws exception when getColumnClass() returns an interface

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Cannot Reproduce
    • Icon: P4 P4
    • None
    • 6u10
    • client-libs

      FULL PRODUCT VERSION :
      java version "1.6.0_13"
      Java(TM) SE Runtime Environment (build 1.6.0_13-b03)
      Java HotSpot(TM) Client VM (build 11.3-b02, mixed mode, sharing)


      ADDITIONAL OS VERSION INFORMATION :
      Microsoft Windows XP [Version 5.1.2600]


      A DESCRIPTION OF THE PROBLEM :
      In a JTable, if I specify an interface for a column class, the JTable will
      throw a NullPointerException when it tries to render that column. However,
      there are two exceptions to this bug. First, if I also specify a Renderer for
      the interface (using setDefaultRenderer), it will use that renderer. Second,
      the Icon interface already has a renderer specified (except in Nimbus), so
      Icons work properly. (The Nimbus L&F removes all the default renderers,
      making the Icon interface fail, just like any other interface.)


      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the test case. Hit the first button, which uses specified a class for
      the column class. It will render fine. Then hit the second button, which
      specified an interface for the column class. This won't render, and will
      throw a NullPointerException in prepareRenderer. (The third button shows
      the workaround.)


      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The table should render properly regardless of whether the column class is
      a class or an interface.

      ACTUAL -
      When I specify an interface, the table throws an exception when it tries
      to render the cell.


      ERROR MESSAGES/STACK TRACES THAT OCCUR :
      Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
      at javax.swing.JTable.prepareRenderer(JTable.java:5729)
      at javax.swing.JTable.getToolTipText(JTable.java:3371)
      at javax.swing.ToolTipManager$insideTimerAction.actionPerformed(ToolTipManager.java:658)
      at javax.swing.Timer.fireActionPerformed(Timer.java:271)
      at javax.swing.Timer$DoPostEvent.run(Timer.java:201)
      at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
      at java.awt.EventQueue.dispatchEvent(EventQueue.java:597)
      at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:269)
      at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:184)
      at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:174)
      at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:169)
      at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:161)
      at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)


      REPRODUCIBILITY :
      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      import java.awt.BorderLayout;
      import java.awt.event.ActionListener;
      import java.awt.event.ActionEvent;
      import javax.swing.*;
      import javax.swing.table.AbstractTableModel;
      import javax.swing.table.TableModel;
      import javax.swing.table.TableCellRenderer;

      @SuppressWarnings({"HardCodedStringLiteral"})
      public class JTableColumnClassBug extends JPanel {
      public static void main(String[] args) {
      JFrame mainFrame = new JFrame("JTableColumnClassBug");
      mainFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
      mainFrame.add(new JTableColumnClassBug());
      mainFrame.pack();
      mainFrame.setVisible(true);
      }

      public JTableColumnClassBug() {
      JButton fineButton = new JButton("Show Table with Class");
      JButton bugButton = new JButton("Show Table with Interface");
      JButton wkArndButton = new JButton("Show Table with Workaround");
      ActionListener alFine = new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      showTable(new BugModelClass(new ThingImpl(5)), "Table with Class");
      }
      };
      fineButton.addActionListener(alFine);
      ActionListener alBug = new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      showTable(new BugModelInterface(new ThingImpl(5)), "Table with Interface");
      }
      };
      bugButton.addActionListener(alBug);
      ActionListener alWorkaround = new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      showWorkaroundTable(new BugModelInterface(new ThingImpl(5)), "Table with Interface, with workaround");
      }
      };
      wkArndButton.addActionListener(alWorkaround);
      add(fineButton);
      add(bugButton);
      add(wkArndButton);
      }

      private void showTable(TableModel model, String title) {
      JFrame tableFrame = new JFrame(title);
      JTable table = new JTable(model);
      showInFrame(tableFrame, table);
      }

      private void showWorkaroundTable(TableModel model, String title) {
      JFrame tableFrame = new JFrame(title);
      JTable table = new JTable(model){

      @Override public TableCellRenderer getDefaultRenderer(Class<?> columnClass) {
      TableCellRenderer cellRenderer = super.getDefaultRenderer(columnClass);
      if (cellRenderer==null) {
      return super.getDefaultRenderer(Object.class);
      }
      return cellRenderer;
      }
      };
      showInFrame(tableFrame, table);
      }

      private void showInFrame(JFrame frame, JTable table) {
      JScrollPane scroller = new JScrollPane(table);
      frame.add(scroller);
      frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
      frame.pack();
      frame.setVisible(true);
      }

      protected class BugModelInterface extends AbstractTableModel {
      private Thing mThing;
      protected BugModelInterface() { }
      BugModelInterface(Thing thing) { mThing = thing; }
      public int getRowCount() { return 1; }
      public int getColumnCount() { return 2; }
      @Override public Class<?> getColumnClass(int columnIndex) {
      if (columnIndex==0)
      return Thing.class;
      return Class.class;
      }
      public Object getValueAt(int rowIndex, int columnIndex) {
      if (columnIndex==0)
      return mThing;
      return getColumnClass(0);
      }


      @Override public String getColumnName(int column) {
      if (column==0)
      return "Value";
      return "Column Class";
      }
      }

      private class BugModelClass extends BugModelInterface {
      private ThingImpl mThing;
      BugModelClass(ThingImpl thing) { mThing = thing; }
      public Class<?> getColumnClass(int columnIndex) {
      if (columnIndex==0)
      return ThingImpl.class;
      return Class.class;
      }

      public Object getValueAt(int rowIndex, int columnIndex) {
      if (columnIndex==0)
      return mThing;
      return getColumnClass(0);
      }
      }

      private interface Thing { public int getValue(); }

      private class ThingImpl implements Thing {
      private final int mValue;
      public ThingImpl(int value) { mValue = value; }

      @Override public String toString() {
      return String.format("V=%d", getValue()); // NON-NLS
      }

      public int getValue() { return mValue; }
      }
      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      The bug is in the method JTable.getDefaultRenderer(). It has a line that's supposed to
      guarantee that a renderer always gets chosen. It looks for a renderer for the
      column class, but if it doesn't find one, it calls itself recursively, like this:

        return getDefaultRenderer(columnClass.getSuperclass());

      The thinking must have been this: it will keep calling itself until the
      getSuperclass() method returns Object.class, for which a renderer is always
       registered. The trouble is that if columnClass is an interface instead of
      a class, then when it gets to one with no superclass, it will return null
      instead of Object.class.

      Consequently, the workaround is to override getDefaultRenderer(), and return
      the renderer for Object.class if it tries to return null. The third button shows
      this workaround in action. Here's the method, from the sample code:

        @Override public TableCellRenderer getDefaultRenderer(Class<?> columnClass) {
          TableCellRenderer cellRenderer = super.getDefaultRenderer(columnClass);
          if (cellRenderer==null) {
            return super.getDefaultRenderer(Object.class);
          }
          return cellRenderer;
        }

      The fix would be to rewrite getDefaultRenderer(), like this:

        public TableCellRenderer getDefaultRenderer(Class<?> columnClass) {
          if (columnClass == null) {
            return null;
          }
          else {
            Object renderer = defaultRenderersByColumnClass.get(columnClass);
            if (renderer != null) {
              return (TableCellRenderer)renderer;
            }
            else {
              superclass = columnClass.getSuperclass(); // Fix starts here
              // If columnClass is an interface...
              if (superclass==null) {
                return getDefaultRenderer(Object.class);
              }
              return getDefaultRenderer(superclass);
            }
          }
        }

      This fix will still return the registered renderer for Icon.class, or any
      other interface for which a renderer was specified. But for unregistered
      interfaces, it will return Object.class instead of null.

            alexp Alexander Potochkin (Inactive)
            ndcosta Nelson Dcosta (Inactive)
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

              Created:
              Updated:
              Resolved:
              Imported:
              Indexed: