Почему мой JXTable сортируется намного медленнее, когда я переключаюсь на EventTableModel GlazedLists?

Обновлено

Я обновил этот вопрос, чтобы более точно описать причину моей проблемы, и включил более простой пример, чем тот, который я первоначально использовал.

Я включил простой пример ниже, чтобы показать проблему с производительностью, с которой я столкнулся. Когда я поддерживаю свой JXTable обычным ArrayList, он работает достаточно хорошо. Однако, если я переключу ArrayList на EventList и построю таблицу с помощью EventTableModel, сортировка будет намного медленнее (в данном случае ~ в 10 раз медленнее).

Если вы используете Maven или Gradle, вот координаты артефакта, которые я использую.

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

И вот пример. Единственная причина, по которой я пытался использовать EventList, заключается в том, что мне нужна была структура данных, которую я мог бы изменить вне TableModel и получить необходимое уведомление.

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

Я также заметил, что таблица, поддерживаемая EventTableModel, сортируется на основе строковых значений, а не числовых значений, но я не уверен, почему. Вот пара скриншотов из профилировщика с сортировкой миллиона строк.

Первая таблица

Вторая таблица

Есть идеи?


person Ryan J    schedule 08.09.2012    source источник
comment
хм ... rowSorter вообще не должен вызываться (сортировка и фильтрация полностью берутся на себя glazedLists). Попробуйте установить для параметра AutoCreateRowSorter значение false, лучше всего перед настройкой модели. Любопытно: в чем причина создания подкласса ComponentProvider вместо его настройки с настроенным FormattedStringValue и выравниванием?   -  person kleopatra    schedule 08.09.2012
comment
@kleopatra setAutoCreateRowSorter(false) не помог, но я понял это. Я создал подкласс ComponentProvider, чтобы добавить некоторые операторы prinln во время отладки. Я обновлю вопрос и опубликую ответ в ближайшее время. Ваши ответы на многих разных сайтах помогли мне несколько раз, так что спасибо.   -  person Ryan J    schedule 10.09.2012


Ответы (1)


Проблема, с которой я столкнулся, заключалась в сочетании того, как SwingX TableRowSorterModelWrapper работает с GlazedLists TableFormat.

При использовании GlazedLists' TableFormat типы классов не предоставляются для столбцов таблицы. Когда тип класса не указан, JXTable в конечном итоге сортирует столбец на основе строковых значений, которые предоставляются ComponentProvider. Если ComponentProvider создается с помощью преобразователя FormatStringValue, каждый элемент в столбце будет отформатирован перед использованием для сравнения во время сортировки. Фактический вызов ComponentProvider происходит в TableRowSorterModelWrapper.

В моем случае, когда я добавил пользовательский рендерер, я заменил ComponentProvider по умолчанию на LabelProvider, который использовал FormatStringValue, который использовал средство форматирования, возвращенное из NumberFormat.getCurrencyInstance().

Причина, по которой таблица, использующая мой SimpleTableModel, не страдала от тех же проблем с производительностью, заключалась в том, что она предоставляла типы классов столбцов. Поскольку BigDecimal реализует Comparable, операции сортировки не требовали вызова ComponentProvider для получения строкового значения (возможно, отформатированного).

Решение очень простое; используйте GlazedLists' AdvancedTableFormat вместо TableFormat и укажите типы классов для каждого столбца таблицы. Следующее будет работать с примером в моем вопросе.

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;
    }
}
person Ryan J    schedule 09.09.2012