Updated
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?