0

In trying to figure out the answer to another question I asked (How to sort a jtable with null values always at the end), I ran into another problem.

I am implementing a custom TableRowSorter that creates a custom Comparator. However, the Comparator always seems to read every Object as a type String. This one has me baffled.

If you'll notice in the SSCCE below, the line

System.out.println(o1.getClass() + " - " + o2.getClass());

always yeilds the output

class java.lang.String - class java.lang.String

Even though the items in the Object[][] data array are varied types.

import java.awt.Component;
import java.util.Comparator;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;

public class Test {

    public static void main(String args[]) {
        JFrame frame = new JFrame();
        JTable table = new JTable();
        Object[][] data = new Object[8][3];
        data[0][0] = 6.5d; data[0][1] = "Name1";
        data[1][0] = new NullClassFiller(); data[1][1] = "Name2";
        data[2][0] = 2.6d; data[2][1] = "Name3";
        data[3][0] = 0d; data[3][1] = "Name4";
        data[4][0] = new NullClassFiller(); data[4][1] = "Name5";
        data[5][0] = -4d; data[5][1] = "Name6";
        data[6][0] = 0d; data[6][1] = "Name7";
        data[7][0] = -4.3d; data[7][1] = "Name8";
        table.setModel(new DefaultTableModel(data, new String[]{"One", "Two"}));

        TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel()) {
            @Override
            public Comparator<?> getComparator(final int column) {
                Comparator c = new Comparator() {
                    @Override
                    public int compare(Object o1, Object o2) {
                        System.out.println(o1.getClass() + " - " + o2.getClass());
                        if (o1 instanceof NullClassFiller) {
                            return -1;
                        } else if (o2 instanceof NullClassFiller) {
                            return -1;
                        } else {
                            return ((Comparable<Object>) o1).compareTo(o2);
                        }

                    }
                };
                return c;
            }
        };
        table.setRowSorter(sorter);
        table.getColumnModel().getColumn(0).setCellRenderer(new CustomRenderer());
        JScrollPane pane = new JScrollPane(table);
        frame.add(pane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setSize(500, 500);
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    static class NullClassFiller {}

    static class CustomRenderer extends DefaultTableCellRenderer {

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            DefaultTableCellRenderer renderer = (DefaultTableCellRenderer) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

            if(value instanceof NullClassFiller)
                renderer.setText("");

            return renderer;
        }

    }
}
Community
  • 1
  • 1
ryvantage
  • 13,064
  • 15
  • 63
  • 112

3 Answers3

3

There are number of compound problems...

The first is with the DefaultTableModel. DefaultTableModel#getColumnClass returns Object.class.

The second is with the TableRowSorter. TableRowSorter checks to see if the Class returned from the model is Comparable, if it is not, it is automatically converted to String, because Object is not Comparable...

So, the basic solution is to override the getColumnClass of the DefaultTableModel to return the appropriate type of Class of a particular column

TableModel model = new DefaultTableModel(data, new String[]{"One", "Two"}) {
    @Override
    public Class<?> getColumnClass(int columnIndex) {
        return columnIndex == 0 ? Double.class : String.class;
    }
};
table.setModel(model);

When the TableRowSorter checks the column's Class it will now find Comparable values and will use the actual value from the table model and not convert it to String first.

Now when you try and sort the first column, you should be seeing something more like...

class testtablesort.TestTableSort$NullClassFiller) - class java.lang.Double
class java.lang.Double) - class testtablesort.TestTableSort$NullClassFiller
class java.lang.Double) - class java.lang.Double
class testtablesort.TestTableSort$NullClassFiller) - class java.lang.Double
class java.lang.Double) - class testtablesort.TestTableSort$NullClassFiller
class java.lang.Double) - class java.lang.Double
class java.lang.Double) - class java.lang.Double
class java.lang.Double) - class testtablesort.TestTableSort$NullClassFiller
class java.lang.Double) - class java.lang.Double
class java.lang.Double) - class java.lang.Double
class java.lang.Double) - class java.lang.Double
class java.lang.Double) - class java.lang.Double
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Yes, DefaultTableModel documentation [states](http://docs.oracle.com/javase/7/docs/api/javax/swing/table/DefaultTableModel.html): `Warning: DefaultTableModel returns a column class of Object. When DefaultTableModel is used with a TableRowSorter this will result in extensive use of toString, which for non-String data types is expensive. If you use DefaultTableModel with a TableRowSorter you are strongly encouraged to override getColumnClass to return the appropriate type.` – Sergej Panic Sep 08 '13 at 07:27
  • while not-overriding getColumnClass is the usual culprit, strictly speaking (nitpicking Sunday morning again :-), it's not applicable here: the column cells have content of _different_ types. Could be remedied by adding null instead of the NullCellValue (if that particular marker isn't needed for other reasons) – kleopatra Sep 08 '13 at 07:37
  • @kleopatra Agreed, but this gets us over the first issue and as you say, simply supplying null would solve the second – MadProgrammer Sep 08 '13 at 07:38
  • not really, as this problem is only the consequence of a hack around the original requirement (as seen in the OP's original question). With the hack (not using it wouuld be best ;-), the column data are of mixed type with not common ancestor except Object. – kleopatra Sep 08 '13 at 08:18
  • Thank you all for your feedback. @MadProgrammer, yours was the most helpful answer to this specific question and it helped me solve the original problem. So, thank you. The answer to the original problem can be found here (http://stackoverflow.com/questions/18680402/how-to-sort-a-jtable-with-null-values-always-at-the-end/18684189#18684189) – ryvantage Sep 08 '13 at 13:26
1

There are several problematic aspects, both with the question and some answers:

  • the real requirement is to have null values sorted at end of the table, irrespective of sortOrder
  • a clean solution of that requirement requires a near-to complete re-write of DefaultRowSorter and its subclasses because nulls are handled "early" in the comparing algorithem and is buried deep inside its private bowels. Simply no way a custom comparator could jump in.
  • a dirty hack around is to not add nulls, but a dedicated null-substitute and then let a custom comparator do its job. Part of the dirtyness is that the comparator must know the sortOrder, that is ultimately needs a handle to the calling RowSorter.
  • hacks are ... evil: even if we go for them (don't, don't, don't ...) they are brittle and we have to take extreme care to get them right at least in very controlled environments!

Here the hack is implemented incorrectly in several respects, the comparator

  • is incomplete, not handling the comparison of two nullFillers
  • doesn't even has the meat of the hack (that is, toggle the returned value based on the sortOrder if a nullFiller is one of the values to compare)
  • collides with the other implementation details of the DefaultRowSorter by overriding getComparator(int) vs. calling setComparator(int, Comparator)

So if anybody decides to apply a hack s/he finds somewhere, at least copy it correctly ;-)

Community
  • 1
  • 1
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • the `getComparator()` method needed to be overwritten to have access to `getSortKeys()` of the `TableRowSorter`. I removed most of that for the sake of the SSCCE, maybe I should've left it in to show where I was going with the solution to the original question. :) But anyways, I can't see another way.. – ryvantage Sep 08 '13 at 13:41
  • Also, can you clarify what a "hack" is? I mean, we bend code to our whims all the time, the solution I ended up with doesn't seem that bad to me... Maybe a little confusing, but it works. – ryvantage Sep 08 '13 at 13:42
  • @ryvantage the hack here is the dummy value - it's not really part of the model. Having it leads to tons of subsequent problems, f.i. made the model implementation in your self-answer in the other question incorrect. And no, we must not bend code whimsically, that's always a path into desaster in the long run ;-) – kleopatra Sep 08 '13 at 14:30
  • Well, @kleopatra, I've been working for 4 years now as a self-employed software contractor so I will readily admit that I'm no stranger to disaster code. (I'm still finding `catch { e.printStackTrace() }`s in my earliest production code). So I'm always ready/hopeful/eager/dead-set on figuring out how to do things the right way. – ryvantage Sep 08 '13 at 19:03
0

The problem is here:

table.setModel(new DefaultTableModel(data, new String[]{"One", "Two"}));

According to http://docs.oracle.com/javase/tutorial/uiswing/components/table.html,

There are two JTable constructors that directly accept data (SimpleTableDemo uses the first):

JTable(Object[][] rowData, Object[] columnNames) JTable(Vector rowData, Vector columnNames) The advantage of these constructors is

that they are easy to use. However, these constructors also have disadvantages:

They automatically make every cell editable. They treat all data types the same (as strings).

u3l
  • 3,342
  • 4
  • 34
  • 51
  • *"DefaultTableModel always treats all data types as Strings by default"* I don't find that statement to be true. `AbstractTableModel#getColumnClass` returns `Object.class` and isn't modified by `DefaultTableModel`. If you do `defaultTableModel.getValueAt(0, 0)` I'm pretty sure you will get a `Double` back - otherwise it would be pointless allowing the `DefaultTableModel` accept anything other than `String` values for it's data – MadProgrammer Sep 08 '13 at 06:57
  • According to [link](http://docs.oracle.com/javase/tutorial/uiswing/components/table.html): `JTable(Object[][] rowData, Object[] columnNames) The advantage of these constructors is that they are easy to use. However, these constructors also have disadvantages: They automatically make every cell editable. They treat all data types the same (as strings).` Since JTable(Object[][] rowData, Object[] columnNames) uses DefaultTableModel I assumed that DefaultTableModel treats Objects as Strings. I'm probably wrong though in hindsight, and your answer seems more appropriate. – u3l Sep 08 '13 at 07:15
  • 1st thank you for the clarification, it sounded like you were suggesting that the DefaultTableModel was converting the data to String some how. 2nd I'd suggest adding that quote to your answer so old fools like me don't mistaken your intention. 3rd This could be changed by suppling a default renderer or editor for Object and or overriding the getColumnClass method of the table model, this changing that statement - but I'm just being fickle now ;) – MadProgrammer Sep 08 '13 at 07:43
  • unfortunately, your edit detoriated your answer considerably - at first it was only a misunderstanding of the (concededly suboptimal) tutorial, now it's doing something completely **wrong* (at least since jdk6), in moving the sorting into the model - it's the view's responsibility and sorting will happen automagically when the model is changed – kleopatra Sep 08 '13 at 07:45
  • Alright, i'll add the clarification. Thank you kleopatra, i'll remove that. – u3l Sep 08 '13 at 08:01