Wie kann man Tabellenspalten nach eigenen Kriterien sortieren?

Das Beispiel demonstriert, wie sich durch das Anlegen von Comparator-Klassen Tabelleneinträge durch Klicken auf den Tabellenkopf nach individuellen Kriterien sortieren lassen. Die durch JTable unterstützte auf- oder absteigende Sortierung durch Klick auf eine Spalte im Tabellenkopf bleibt dabei erhalten.

Das Beispiel

Das Beispiel zeigt eine einfache, erdachte Tabelle mit Einträgen von Bezeichnungen, Seriennummern und Preisen von Haushaltsgegenständen. Es demonstriert, wie bei den drei Spalten unterschiedliche Sortierungen eingerichtet werden können, sodass durch Klick auf den Spaltenkopf der Inhalt der jeweiligen Spalte wechselweise auf- und absteigend sortiert wird.
Darüber hinaus wird gezeigt, wie die Spalten eines Tabellenkopfs mit verschiedenen Tooltips versehen werden können.

Das GUI

Die Klasse SpalteSortieren ist von einem JFrame abgeleitet und liefert die grafische Oberfläche des Programms. Das Fenster zeigt eine einfache JTable, die mit drei Spalten und fünf Reihen gefüllt ist, in denen Haushaltsgegenstände mit einer Seriennummer und ihrem Preis aufgelistet sind.
Die Oberfläche wird durch die Methode initGUI() erzeugt, die im Konstruktor aufgerufen wird.

private void initGUI() {
    DefaultTableModel model = createModel();
    JTable table = new JTable(model);
    table.setShowGrid(true);
    table.setGridColor(Color.GRAY);
    JTableHeader header = createTableHeader(table.getColumnModel());
    header.setBackground(Color.LIGHT_GRAY);
    table.setTableHeader(header);
    table.setAutoCreateRowSorter(true);
    TableRowSorter<DefaultTableModel> newSorter = new TableRowSorter<DefaultTableModel>(
            model);
    header.addMouseListener(new MouseAdapter() {
        public void mouseReleased(MouseEvent e) {
            sortColumns(e, table, newSorter);
        }
    });
    this.add(new JScrollPane(table), BorderLayout.CENTER);
}

//...

private DefaultTableModel createModel() {
    String[] header = { "Objekt", "Seriennummer", "Preis" };
    String[][] data = {
            new String[] { "B\u00FCgeleisen", "kg-04-7385", "68,98" },
            new String[] { "Staubsauger", "24.17-123456", "417" },
            new String[] { "Eierkocher", "eikoch-9413", "19,95" },
            new String[] { "Luftpumpe", "0034-lp-1713", "19,95" },
            new String[] { "Schreibtisch", "x24-scht-0815", "235,50" } };
    return new DefaultTableModel(data, header);
}

Hier wird zunächst ein DefaultTableModel durch die Methode createModel() erzeugt, das die Inhalte des Tabellen-Views verwaltet. Es umfasst ein einfaches String-Array für den Tabellenkopf und ein zweidimensionales String-Array des Tabelleninhaltes.
Die Tabelle wird mit dem Model initialisiert und mit einem Raster und der Linienfarbe Grau versehen. Letzteres geschieht, da die Standard-Linienfarbe Weiß ist, sodass das Raster bei weißem Hintergrund nicht zu sehen wäre.

Es folgt die Einrichtung des Tabellenkopfes, der durch die Methode createTableHeader() zurückgegeben und durch JTable#setTableHeader() der Tabelle hinzugefügt wird. Das Verfahren ist notwendig, um dem Tabellenkopf die Tooltips hinzuzufügen.

Tooltips auf dem Tabellenkopf

In createTableHeader() wird ein Objekt vom Typ JTableHeader durch Übergabe des TableColumnModel der Tabelle erzeugt. In der anonymen Klasse wird dort die Methode JTableHeader#getToolTipText(MouseEvent e) überschrieben.

private JTableHeader createTableHeader(TableColumnModel cm) {
    return new JTableHeader(cm) {
        public String getToolTipText(MouseEvent e) {
            Point p = e.getPoint();
            int ci = cm.getColumnIndexAtX(p.x);
            int mi = cm.getColumn(ci).getModelIndex();
            return createToolTips()[mi];
        }
    };
}

Um den Spaltenkopf zu registrieren, wird durch das übergebene MouseEvent die Position des Cursors über ein Point-Objekt bestimmt und aus dem ColumnModel dann der Index der zugehörigen Spalte ermittelt.
Die Tooltips selbst werden in einer gesonderten Methode, createToolTips(), als String-Array erzeugt und über den Spaltenindex aufgerufen. Nebenbei wird hier beim Tooltip für die mittlere Spalte demonstriert, dass Tooltips in Java auch mit HTML formatiert werden können.

private String[] createToolTips() {
    return new String[] {
        "Click sortiert lexikalisch",
        "<html>Click sortiert lexikalisch,<br>Alt-Click sortiert nach der Endnummer</html>",
        "Click sortiert numerisch"
    };
}

Die Sortierung durch RowSorter

Die Methode JTable#setAutoCreateRowSorter(true) ermöglicht durch die Übergabe des Parameters true ein automatisches Sortieren der Tabelle dergestalt, dass durch Klick auf den Kopf einer Spalte diese lexikalisch sortiert wird. Wiederholtes Klicken führt zur gegenläufigen Sortierung. Intern wird hierdurch der Tabelle ein Objekt vom Typ TableRowSorter hinzugefügt. Die Belegung des Inhalts der restlichen Tabelle wird bei der Sortierung entsprechend angepasst, sodass die Konsistenz der Tabellenreihen gewahrt wird.

Die Sortierung erfolgt durch abstrakte RowSorter, die durch die Klasse TableRowSorter konkretisiert werden. Ein Objekt vom Typ TableRowSorter<DefaultTableModel>() wird erstellt und diesem das aktuelle TableModel als Gegenstand der Sortierung übergeben. Zum Schluss wird der Tabellenkopf bei einem MouseListener angemeldet, die Tabelle einem JScrollPane hinzugefügt und dies mittig auf das Programmfenster gesetzt.

private void sortColumns(MouseEvent e, JTable table, TableRowSorter<DefaultTableModel> newSorter) {
    int spalte = table.columnAtPoint(e.getPoint());
    String spaltenName = table.getColumnName(spalte);
    table.setRowSorter(newSorter);
    if (spaltenName.equals("Seriennummer") && e.isAltDown()) {
        newSorter.setComparator(spalte, new NumberSorter("-"));
        newSorter.sort();
    } else if (spaltenName.equals("Preis")) {
        newSorter.setComparator(spalte, new DecimalSorter());
        newSorter.sort();
    }
}

Der erwähnte MouseListener des Tabellenkopfes ist im Beispiel durch einen MouseAdapter realisiert, da hier lediglich die Reaktion auf mouseReleased() benötigt wird. Hier wird die Methode sortColumns() aufgerufen, die die gesamte Routine für die Sortierung enthält. Ihr werden hierzu das MouseEvent, das Tabellen-Objekt und der neu gebildete TableRowSorter übergeben.

Die Methode ermittelt zunächst den Spaltenindex und den Spaltennamen über das MouseEvent. Nachdem der neu gebildete TableRowSorter auf die Tabelle gesetzt wurde, wird mittels einer Verzweigung anhand der Spaltennamen zwischen den Spalten unterschieden und den Sortern Objekte verschiedener Comparator-Klassen zugewiesen. Sie dienen dem konkreten Vergleich der Einzeleinträge und ermöglichen so erst die Sortierung. Für die Spalten "Seriennummer" und "Preis" werden hier gesonderte, unten erläuterte Klassen verwendet. Für die verbleibende Spalte "Objekt" wird die lexikalische Standard-Sortierung beibehalten.
Um die numerische Sortierung der Spalte "Seriennummer" zu aktivieren, muss zusätzlich die Alt-Taste gedrückt werden, ansonsten findet auch hier eine lexikalische Sortierung statt. Es ist wichtig, nach der Zuweisung jeweils die Methode DefaultRowSorter#sort() aufzurufen, um die Sortierung anzustoßen.

Die Comparator-Klassen

Comparator-Klassen dienen dem Vergleich zweier Werte. Sie implementieren das Interface Comparator und müssen somit die Methode compare() konkretisieren, in der der Vergleichsalgorithmus implementiert wird.

NumberSorter

Die Klasse dient dazu, Strings, die mit einer Zahl enden, in der Reihenfolge dieser Endungen zu sortieren. Im Beispiel besitzt jedes Objekt in der mittleren Spalte eine Seriennummer, die, getrennt durch ein '-', am Ende jeweils eine Nummer mit unterschiedlicher Stellenzahl besitzt. Eine lexikalische Sortierung kommt daher nicht in Frage, wenn die Zahlenwerte der Größe nach erfasst werden sollen. Der Zahlenanteil des Strings muss also zunächst separiert, dann als Zahl interpretiert und schließlich sortiert werden.

class NumberSorter implements Comparator<String> {

    final String trenner;

    public NumberSorter(final String trenner) {
        this.trenner = trenner;
    }

    @Override
    public int compare(String s1, String s2) {
        int pos1 = s1.lastIndexOf(trenner) + 1;
        int pos2 = s2.lastIndexOf(trenner) + 1;
        if (pos1 > -1) {
            s1 = ((String) s1).substring(pos1);
        }
        if (pos2 > -1) {
            s2 = ((String) s2).substring(pos2);
        }
        if (s1 instanceof String && ((String) s1).length() == 0) {
            s1 = null;
        }
        if (s2 instanceof String && ((String) s2).length() == 0) {
            s2 = null;
        }
        if (s1 == null && s2 == null) {
            return 0;
        } else if (s1 == null) {
            return 1;
        } else if (s2 == null) {
            return -1;
        } else {
            try {
                int i1 = new Integer(s1).intValue();
                int i2 = new Integer(s2).intValue();
                int erg = i1 < i2 ? 1 : i1 > i2 ? -1 : 0;
                return erg;
            } catch (NumberFormatException e) {
            }
        }
        return ((String) s1).compareTo((String) s2);
    }
}

Das Trennzeichen wird dem Konstruktor der Comparator-Klasse übergeben, da die Signatur von compare() selbst nicht geändert werden kann. In der Methode werden die numerischen Anteile zunächst durch String#substring() ermittelt und dann, nach einigen Sicherheitsprüfungen, in Integer gewandelt. Diese können in primitive int-Werte unboxed und anschließend verglichen werden. Durch den Vergleich müssen ganzzahlige Werte größer 0, kleiner 0 oder bei Gleichheit gleich 0 zurückgegeben werden.

DecimalSorter

Die Klasse DecimalSorter dient dazu, die in der Spalte "Preis" angegebenen Zahlen zu sortieren, da auch hier eine lexikalische Sortierung nicht zum gewünschten Ergebnis führen würde.
Auch hier findet eine Wandlung der zur Sortierung anstehenden Strings in numerische Werte statt. Allerdings sind die Dezimalzahlen mit einem Komma als Dezimaltrenner versehen und müssen in double gewandelt werden. Die Sortierung durch Vergleich erfolgt dann auf eine ähnliche Weise wie oben.

class DecimalSorter implements Comparator<String> {

    @Override
    public int compare(String o1, String o2) {
        Double d1 = convertToDouble(o1);
        Double d2 = convertToDouble(o2);
        if (d1 == null && d2 == null) {
            return 0;
        } else if (d1 == null) {
            return 1;
        } else if (d2 == null) {
            return -1;
        }
        return d1 < d2 ? 1 : d2 < d1 ? -1 : 0;
    }

    private Double convertToDouble(String s) {
        if (s.indexOf(",") > -1)
            s = s.replace(",", ".");
        if (s.matches("-?\\d+([.]{1}\\d+)?")) {
            try {
                return new Double(s);
            } catch (NumberFormatException e) {
            }
        }
        return null;
    }
}

Zuvor jedoch müssen die Spaltenwerte in der Methode convertToDouble() konvertiert werden. Dies geschieht in zwei Schritten: Zunächst werden die Kommata durch Punkte ersetzt. Es findet dann eine Prüfung über einen regulären Ausdruck statt, der sicherstellt, dass es sich bei dem geprüften String um einen ganzzahligen oder dezimalen Zahlenwert handelt. Anschließend finden die Konvertierung zu Double und dessen Rückgabe statt.
Der eigentliche Vergleich findet in compare() schließlich wieder über einen Größenvergleich statt.

Vollständiger Quelltext

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Comparator;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableRowSorter;

public class SpalteSortieren extends JFrame {

    public SpalteSortieren() {
        initGUI();
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setTitle("Tabelle nach Spalten sortieren");
        this.setSize(600, 400);
        this.setLocationRelativeTo(null);
        this.setVisible(true);
    }

    private void initGUI() {
        DefaultTableModel model = createModel();
        JTable table = new JTable(model);
        table.setShowGrid(true);
        table.setGridColor(Color.GRAY);
        JTableHeader header = createTableHeader(table.getColumnModel());
        header.setBackground(Color.LIGHT_GRAY);
        table.setTableHeader(header);
        table.setAutoCreateRowSorter(true);
        TableRowSorter<DefaultTableModel> newSorter = new TableRowSorter<DefaultTableModel>(
                model);
        header.addMouseListener(new MouseAdapter() {
            public void mouseReleased(MouseEvent e) {
                sortColumns(e, table, newSorter);
            }
        });
        this.add(new JScrollPane(table), BorderLayout.CENTER);
    }

    private void sortColumns(MouseEvent e, JTable table, TableRowSorter<DefaultTableModel> newSorter) {
        int spalte = table.columnAtPoint(e.getPoint());
        String spaltenName = table.getColumnName(spalte);
        table.setRowSorter(newSorter);
        if (spaltenName.equals("Seriennummer") && e.isAltDown()) {
            newSorter.setComparator(spalte, new NumberSorter("-"));
            newSorter.sort();
        } else if (spaltenName.equals("Preis")) {
            newSorter.setComparator(spalte, new DecimalSorter());
            newSorter.sort();
        }
    }

    private JTableHeader createTableHeader(TableColumnModel cm) {
        return new JTableHeader(cm) {
            public String getToolTipText(MouseEvent e) {
                Point p = e.getPoint();
                int ci = cm.getColumnIndexAtX(p.x);
                int mi = cm.getColumn(ci).getModelIndex();
                return createToolTips()[mi];
            }
        };
    }

    private String[] createToolTips() {
        return new String[] {
                "Click sortiert lexikalisch",
                "<html>Click sortiert lexikalisch,<br>Alt-Click sortiert nach der Endnummer",
                "Click sortiert numerisch" };
    }

    private DefaultTableModel createModel() {
        String[] header = { "Objekt", "Seriennummer", "Preis" };
        String[][] data = {
                new String[] { "B\u00FCgeleisen", "kg-04-7385", "68,98" },
                new String[] { "Staubsauger", "24.17-123456", "417" },
                new String[] { "Eierkocher", "eikoch-9413", "19,95" },
                new String[] { "Luftpumpe", "0034-lp-1713", "19,95" },
                new String[] { "Schreibtisch", "x24-scht-0815", "235,50" } };
        return new DefaultTableModel(data, header);
    }

    public static void main(String[] args) {
        new SpalteSortieren();
    }
}

class NumberSorter implements Comparator<String> {

    final String trenner;

    public NumberSorter(final String trenner) {
        this.trenner = trenner;
    }

    @Override
    public int compare(String s1, String s2) {
        int pos1 = s1.lastIndexOf(trenner) + 1;
        int pos2 = s2.lastIndexOf(trenner) + 1;
        if (pos1 > -1) {
            s1 = ((String) s1).substring(pos1);
        }
        if (pos2 > -1) {
            s2 = ((String) s2).substring(pos2);
        }
        if (s1 instanceof String && ((String) s1).length() == 0) {
            s1 = null;
        }
        if (s2 instanceof String && ((String) s2).length() == 0) {
            s2 = null;
        }
        if (s1 == null && s2 == null) {
            return 0;
        } else if (s1 == null) {
            return 1;
        } else if (s2 == null) {
            return -1;
        } else {
            try {
                int i1 = new Integer(s1).intValue();
                int i2 = new Integer(s2).intValue();
                int erg = i1 < i2 ? 1 : i1 > i2 ? -1 : 0;
                return erg;
            } catch (NumberFormatException e) {
            }
        }
        return ((String) s1).compareTo((String) s2);
    }
}

class DecimalSorter implements Comparator<String> {

    @Override
    public int compare(String o1, String o2) {
        Double d1 = convertToDouble(o1);
        Double d2 = convertToDouble(o2);
        if (d1 == null && d2 == null) {
            return 0;
        } else if (d1 == null) {
            return 1;
        } else if (d2 == null) {
            return -1;
        }
        return d1 < d2 ? 1 : d2 < d1 ? -1 : 0;
    }

    private Double convertToDouble(String s) {
        if (s.indexOf(",") > -1)
            s = s.replace(",", ".");
        if (s.matches("-?\\d+([.]{1}\\d+)?")) {
            try {
                return new Double(s);
            } catch (NumberFormatException e) {
            }
        }
        return null;
    }
}

Screenshot des Tooltips auf der mittleren Spalte

Quellen

http://docs.oracle.com/javase/tutorial/uiswing/components/table.html#headertooltip