I have updated this question to more accurately describe the cause of my problem and have included a simpler example that the one I originally used.
I've included a simple example below to show the performance issue I'm having. When I back my JXTable with a normal ArrayList, it performs reasonably well. However, if I switch the ArrayList for an EventList and build the table using an EventTableModel, the sorting is much slower (~10x slower in this case).
If using Maven or Gradle, here are the artifact coordinates I'm using.
apply plugin: 'java'
apply plugin: 'application'
mainClassName = "SortPerfMain"
dependencies {
compile "net.java.dev.glazedlists:glazedlists_java15:1.8.0"
compile "org.swinglabs.swingx:swingx-core:1.6.4"
}
And here is the example. The only reason I was trying to use an EventList is because I wanted a data structure that I could modify outside of the TableModel and have the necessary notification occur.
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.swing.EventTableModel;
import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.renderer.*;
import org.jdesktop.swingx.table.TableColumnExt;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import static javax.swing.WindowConstants.EXIT_ON_CLOSE;
/* This class creates a JFrame with two JXTables displayed side by side. Both
* tables have a single column that holds Item objects. Each Item has one
* property; amount. The amount property is a BigDecimal, but the performance
* disparity is still present when using int instead.
*
* The first table is backed by a simple ArrayList. The second table is backed
* by an EventList (GlazedLists).
*
* When sorting 1,000,000 rows, the first table takes about 1 second and the
* second table takes about 10 seconds.
*/
public class SortPerfMain {
@SuppressWarnings("FieldCanBeLocal")
private final boolean useDebugRenderer = true;
// The number of items that should be added to the model.
@SuppressWarnings("FieldCanBeLocal")
private final int itemCount = 2;
// The number of visible rows in each table.
@SuppressWarnings("FieldCanBeLocal")
private final int visibleRowCount = 2;
public static void main(String[] args) {
new SortPerfMain();
}
public SortPerfMain() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
List<Item> itemList = createItemList();
JPanel leftPanel = createTablePanel(
createTable(createSimpleModel(itemList)));
JPanel rightPanel = createTablePanel(
createTable(createGlazedModel(itemList)));
JPanel mainPanel = new JPanel(new GridLayout(1, 2));
mainPanel.add(leftPanel);
mainPanel.add(rightPanel);
JFrame mainFrame = new JFrame("Table Sort Perf");
mainFrame.setContentPane(mainPanel);
mainFrame.pack();
mainFrame.setSize(600, mainFrame.getHeight());
mainFrame.setLocationRelativeTo(null);
mainFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
mainFrame.setVisible(true);
}
});
}
private List<Item> createItemList() {
List<Item> itemList = new ArrayList<>(itemCount);
for (int i = 0; i < itemCount; i++) {
itemList.add(new Item(i));
}
return itemList;
}
private JXTable createTable(TableModel model) {
JXTable table = new JXTable(model);
table.setVisibleRowCount(visibleRowCount);
addRenderer(table);
return table;
}
private void addRenderer(JXTable table) {
TableColumnExt column = table.getColumnExt(Columns.AMOUNT.ordinal());
column.setCellRenderer(createCurrencyRenderer());
}
private JPanel createTablePanel(JXTable table) {
JLabel panelLabel = new JLabel(table.getModel().getClass().getName());
JPanel panel = new JPanel(new BorderLayout());
panel.add(panelLabel, BorderLayout.NORTH);
panel.add(new JScrollPane(table), BorderLayout.CENTER);
return panel;
}
private TableModel createSimpleModel(List<Item> items) {
return new SimpleTableModel(items);
}
private TableModel createGlazedModel(List<Item> items) {
EventList<Item> itemList = new BasicEventList<>();
itemList.addAll(items);
return new EventTableModel<>(itemList, new EventTableModelFormat());
}
private TableCellRenderer createCurrencyRenderer() {
//noinspection ConstantConditions
if (useDebugRenderer) {
return new DebugRenderer();
}
return new DefaultTableRenderer(
new LabelProvider(new FormatStringValue(
NumberFormat.getCurrencyInstance())));
}
// Enum for managing table columns
private static enum Columns {
AMOUNT("Amount", BigDecimal.class);
private final String name;
private final Class type;
private Columns(String name, Class type) {
this.name = name;
this.type = type;
}
}
// Each table holds a list of items.
private static class Item {
private final BigDecimal amount;
private Item(BigDecimal amount) {
this.amount = amount;
}
private Item(int amount) {
this(new BigDecimal(amount));
}
}
// A simple model that doesn't perform any change notification
private static class SimpleTableModel extends DefaultTableModel {
private final List<Item> itemList;
public SimpleTableModel(List<Item> items) {
this.itemList = items;
}
@Override
public int getRowCount() {
if (itemList == null) {
return 0;
}
return itemList.size();
}
@Override
public int getColumnCount() {
return Columns.values().length;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
switch (Columns.values()[columnIndex]) {
case AMOUNT:
return itemList.get(rowIndex).amount;
}
return null;
}
@Override
public String getColumnName(int column) {
return Columns.values()[column].name;
}
@Override
public Class<?> getColumnClass(int column) {
return Columns.values()[column].type;
}
}
// Table format for use with the EventTableModel
private static class EventTableModelFormat implements TableFormat<Item> {
@Override
public int getColumnCount() {
return 1;
}
@Override
public String getColumnName(int i) {
return Columns.values()[i].name;
}
@Override
public Object getColumnValue(Item item, int i) {
return item.amount;
}
}
/* The following classes are used to add println statements to the part
* of the component hierarchy we're interested in for debugging.
*/
private class DebugRenderer extends DefaultTableRenderer {
private DebugRenderer() {
super(new DebugProvider());
}
@Override
public Component getTableCellRendererComponent(
JTable table,
Object value,
boolean isSelected,
boolean hasFocus,
int row,
int column) {
System.out.println("Renderer requested for " + value.toString());
return super.getTableCellRendererComponent(
table, value, isSelected, hasFocus, row, column);
}
}
private class DebugProvider extends LabelProvider {
private DebugProvider() {
super(new DebugFormatter());
}
@Override
public String getString(Object value) {
System.out.println("Providing string for " + value.toString());
return super.getString(value);
}
}
private class DebugFormatter extends FormatStringValue {
private DebugFormatter() {
super(NumberFormat.getCurrencyInstance());
}
@Override
public String getString(Object value) {
System.out.println("Formatting object: " + value.toString());
return super.getString(value);
}
}
}
I also noticed the table backed by the EventTableModel is sorting based on string values rather than numeric values, but I'm not sure why. Here are a couple of screenshots from the profiler with a million rows being sorted.
Any ideas?
The problem I was having with this was a combination of the way SwingX's TableRowSorterModelWrapper
works with GlazedLists' TableFormat
.
When using GlazedLists' TableFormat
the class types are not supplied for table columns. When the class type is not provided, JXTable will end up sorting the column based on string values which are supplied by a ComponentProvider
. If the ComponentProvider
is constructed with a FormatStringValue
converter, every item in the column will be formatted prior to being used for comparison during a sort. The actual call to the ComponentProvider
happens in the TableRowSorterModelWrapper
.
In my case, when I added the custom renderer, I replaced the default ComponentProvider
with a LabelProvider
that was using a FormatStringValue
that was using the formatter returned from NumberFormat.getCurrencyInstance()
.
The reason the table using my SimpleTableModel
didn't suffer from the same performance problems was because it supplied column class types. Since BigDecimal
implements Comparable
, sort operations didn't require a call to the ComponentProvider
to get a (possibly formatted) string value.
The solution is very simple; use GlazedLists' AdvancedTableFormat
instead of TableFormat
and supply the class types for each table column. The following will work with the example in my question.
private static class EventTableModelFormat implements AdvancedTableFormat<Item> {
@Override
public int getColumnCount() {
return 1;
}
@Override
public String getColumnName(int i) {
return Columns.values()[i].name;
}
@Override
public Object getColumnValue(Item item, int i) {
return item.amount;
}
@Override
public Class getColumnClass(int column) {
return Columns.values()[column].type;
}
@Override
public Comparator getColumnComparator(int column) {
return null;
}
}