Java Comparator always reading values as Strings

2019-09-05 19:56发布

问题:

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;
        }

    }
}

回答1:

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


回答2:

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 ;-)



回答3:

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).