¿Por qué mi JXTable ordena mucho más lento cuando cambio a EventTableModel de GlazedLists?

Actualizado

He actualizado esta pregunta para describir con mayor precisión la causa de mi problema y he incluido un ejemplo más simple que el que usé originalmente.

He incluido un ejemplo simple a continuación para mostrar el problema de rendimiento que tengo. Cuando respaldo mi JXTable con un ArrayList normal, funciona razonablemente bien. Sin embargo, si cambio ArrayList por EventList y construyo la tabla usando un EventTableModel, la clasificación es mucho más lenta (~10 veces más lenta en este caso).

Si usa Maven o Gradle, aquí están las coordenadas del artefacto que estoy usando.

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

Y aquí está el ejemplo. La única razón por la que estaba tratando de usar una EventList es porque quería una estructura de datos que pudiera modificar fuera del TableModel y que se produjera la notificación necesaria.

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

También noté que la tabla respaldada por EventTableModel se ordena según los valores de cadena en lugar de los valores numéricos, pero no estoy seguro de por qué. Aquí hay un par de capturas de pantalla del generador de perfiles con un millón de filas ordenadas.

Primera mesa

Segunda mesa

¿Alguna idea?

preguntado el 08 de septiembre de 12 a las 09:09

hmm ... el clasificador de filas no debe llamarse en absoluto (glasedLists se hace cargo por completo de la clasificación y el filtrado). Intente configurar AutoCreateRowSorter en falso, mejor antes de configurar el modelo. Curioso: ¿cuál es el motivo para crear una subclase de ComponentProvider en lugar de configurarlo con un FormattedStringValue y una alineación configurados? -

@kleopatra setAutoCreateRowSorter (falso) no ayudó, pero lo descubrí. Estaba subclasificando ComponentProvider para agregar algunas declaraciones prinln durante la depuración. Actualizaré la pregunta y publicaré una respuesta en breve. Sus respuestas en muchos sitios diferentes me han ayudado en varias ocasiones, así que gracias. -

1 Respuestas

El problema que estaba teniendo con esto era una combinación de la forma en que SwingX TableRowSorterModelWrapper funciona con GlazedLists' TableFormat.

Al usar GlazedLists' TableFormat los tipos de clase no se proporcionan para las columnas de la tabla. Cuando no se proporciona el tipo de clase, JXTable terminará ordenando la columna en función de los valores de cadena proporcionados por un ComponentProvider. Si el ComponentProvider se construye con un FormatStringValue convertidor, cada elemento de la columna se formateará antes de ser utilizado para la comparación durante una ordenación. La llamada real a la ComponentProvider sucede en el TableRowSorterModelWrapper.

En mi caso, cuando agregué el renderizador personalizado, reemplacé el predeterminado ComponentProvider con un LabelProvider que estaba usando un FormatStringValue que estaba usando el formateador devuelto de NumberFormat.getCurrencyInstance().

La razón por la que la tabla usa mi SimpleTableModel no sufrió los mismos problemas de rendimiento porque proporcionó tipos de clase de columna. Ya que BigDecimal implementos Comparable, las operaciones de clasificación no requerían una llamada al ComponentProvider para obtener un valor de cadena (posiblemente formateado).

La solución es muy simple; usar listas esmaltadas' AdvancedTableFormat en lugar de TableFormat y proporcione los tipos de clase para cada columna de la tabla. Lo siguiente funcionará con el ejemplo en mi pregunta.

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

Respondido el 10 de Septiembre de 12 a las 00:09

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.