Метод сравнения нарушает его общий договор - но я бы мог? - PullRequest
0 голосов
/ 06 июня 2018

Я пытаюсь написать сортировщик таблиц, который всегда сортирует нулевые значения.Итак, я написал класс-обёртку, который implements Comparable:

public class WrappedBigDecimal implements Comparable<WrappedBigDecimal> {
    public final BigDecimal value;

    public WrappedBigDecimal(BigDecimal value) {
        this.value = value;
    }

    @Override
    public int compareTo(WrappedBigDecimal o) {
        if (value == null && (o == null || o.value == null)) { // both are null, thus equal
            return 0;
        } else if (value == null && (o != null && o.value != null)) { // value is null and compared value isn't
            return -1;
        } else if (value != null && (o == null || o.value == null)) {
            return 1;
        } else {
            return value.compareTo(o.value);
        }
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }

}

Вы заметите, что метод compareTo выполняет только нулевые проверки, а затем отсылается к методу compareTo упакованного значения.класс.

Затем я написал RowSorter, чей Comparator проверяет SortOrder

public class WrappedNumberSorter extends TableRowSorter<TableModel> {

    public WrappedNumberSorter(TableModel model) {
        super(model);
    }

    @Override
    public Comparator<?> getComparator(final int column) {
        Comparator c = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                boolean ascending = getSortKeys().get(0).getSortOrder() == SortOrder.ASCENDING;

                if (o1 instanceof WrappedBigDecimal && ((WrappedBigDecimal)o1).value == null) {
                    if(ascending)
                        return 1;
                    else
                        return -1;
                } else if (o2 instanceof WrappedBigDecimal && ((WrappedBigDecimal)o2).value == null) {
                    if(ascending)
                        return -1;
                    else
                        return 1;
                } else {
                    return ((Comparable<Object>) o1).compareTo(o2);
                }

            }
        };
        return c;
    }
}  

Но это выдает ошибку (не могу понять почему, не могуВоспроизведите - читайте дальше):

java.lang.IllegalArgumentException: Comparison method violates its general contract!

К счастью, ошибка, похоже, ни на что не влияет, потому что все работает, как задумано.

Я видел этот вопрос ( java.lang.IllegalArgumentException: метод сравнения нарушает его общий контракт ), и я ДУМАЮ, что моя проблема в том, что мое сравнение не является транзитивным.Хотя я не уверен, потому что, если A == B и B == C, то я думаю, что мой также вернется A == C, но меня разворачивают, пытаясь думатьоб этом.

В любом случае, мои вопросы:

  1. Существует ли потенциальная опасность для преднамеренно , если сортировщик строк будет сортировать ноль до дна таким образом?
  2. Это кажется мне переходным.Действительно ли я нарушаю транзитный контракт compareTo здесь?Или есть другой контракт, которого он должен придерживаться, и я не знаю, и это вызывает ошибку?

Я написал метод для проверки сортировщика строк, и я не могу воспроизвести ошибку:

public static void main(String[] args) {
    JFrame frame = new JFrame();
    JTable table = new JTable();
    JScrollPane jsp = new JScrollPane(table);

    String[] headers = new String[] {"h1", "h2"};

    Object[][] data = new Object[10][2];
    for(int i = 0; i < 7; i++) {
        data[i][0] = new WrappedBigDecimal(BigDecimal.TEN.multiply(BigDecimal.valueOf(i % 3 == 0 ? (i*i*-1) : (i*i))));
    }

    data[7][0] = new WrappedBigDecimal(null);
    data[8][0] = new WrappedBigDecimal(null);
    data[9][0] = new WrappedBigDecimal(null);

    for(int i = 10; i < 17; i++) {
        data[i-10][1] = new WrappedBigDecimal(BigDecimal.TEN.multiply(BigDecimal.valueOf(i % 3 == 0 ? (i*i*-1) : (i*i))));
    }

    data[7][1] = new WrappedBigDecimal(null);
    data[8][1] = new WrappedBigDecimal(null);
    data[9][1] = new WrappedBigDecimal(null);

    table.setModel(new DefaultTableModel(data, headers) {
        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            return BigDecimal.class;
        }
    });

    table.setRowSorter(new WrappedNumberSorter(table.getModel()));

    frame.add(jsp);
    frame.setSize(200,400);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

Кажется, все работает гладко.

Чего мне не хватает?

enter image description here


РЕДАКТИРОВАТЬ (7/11/18): Попытка избавиться от решения WrappedBigDecimal и реализовать решение Мэтта МакГенри представляет другую проблему.Я упростил RowSorter до этого:

static class NullsLastSorter extends TableRowSorter<TableModel> {

    public NullsLastSorter(TableModel model) {
        super(model);
    }

    @Override
    public Comparator<?> getComparator(int column) {
        return Comparator.<Optional<BigDecimal>, Boolean>comparing(Optional::isPresent).reversed().thenComparing(o -> o.orElse(BigDecimal.ONE));
    }
}

И протестировал его с:

public static void main(String[] args) {
    JFrame frame = new JFrame();
    JTable table = new JTable();
    JScrollPane jsp = new JScrollPane(table);

    String[] headers = new String[] {"h1", "h2"};

    Object[][] data = new Object[10][2];
    for(int i = 0; i < 7; i++) {
        data[i][0] = BigDecimal.TEN.multiply(BigDecimal.valueOf(i % 3 == 0 ? (i*i*-1) : (i*i)));
    }

    data[7][0] = null;
    data[8][0] = null;
    data[9][0] = null;

    for(int i = 10; i < 17; i++) {
        data[i-10][1] = BigDecimal.TEN.multiply(BigDecimal.valueOf(i % 3 == 0 ? (i*i*-1) : (i*i)));
    }

    data[7][1] = null;
    data[8][1] = null;
    data[9][1] = null;



    table.setModel(new DefaultTableModel(data, headers) {
        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            return BigDecimal.class;
        }
    });

    table.setRowSorter(new NullsLastSorter(table.getModel()));

    frame.add(jsp);
    frame.setSize(200,400);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

И теперь, когда я пытаюсь отсортировать, я получаю ошибку:

Exception in thread "AWT-EventQueue-0" java.lang.ClassCastException: java.math.BigDecimal cannot be cast to java.util.Optional
at java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:469)
at java.util.Collections$ReverseComparator2.compare(Collections.java:5178)
at java.util.Comparator.lambda$thenComparing$36697e65$1(Comparator.java:216)
at javax.swing.DefaultRowSorter.compare(DefaultRowSorter.java:968)
at javax.swing.DefaultRowSorter.access$100(DefaultRowSorter.java:112)
at javax.swing.DefaultRowSorter$Row.compareTo(DefaultRowSorter.java:1376)
at javax.swing.DefaultRowSorter$Row.compareTo(DefaultRowSorter.java:1366)
at java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
at java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
at java.util.Arrays.sort(Arrays.java:1246)
at javax.swing.DefaultRowSorter.sort(DefaultRowSorter.java:607)
at javax.swing.DefaultRowSorter.setSortKeys(DefaultRowSorter.java:319)
at javax.swing.DefaultRowSorter.toggleSortOrder(DefaultRowSorter.java:480)
at javax.swing.plaf.basic.BasicTableHeaderUI$MouseInputHandler.mouseClicked(BasicTableHeaderUI.java:112)
at java.awt.AWTEventMulticaster.mouseClicked(AWTEventMulticaster.java:270)
at java.awt.Component.processMouseEvent(Component.java:6536)
at javax.swing.JComponent.processMouseEvent(JComponent.java:3324)
at java.awt.Component.processEvent(Component.java:6298)
at java.awt.Container.processEvent(Container.java:2236)
at java.awt.Component.dispatchEventImpl(Component.java:4889)
at java.awt.Container.dispatchEventImpl(Container.java:2294)
at java.awt.Component.dispatchEvent(Component.java:4711)
at java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4888)
at java.awt.LightweightDispatcher.processMouseEvent(Container.java:4534)
at java.awt.LightweightDispatcher.dispatchEvent(Container.java:4466)
at java.awt.Container.dispatchEventImpl(Container.java:2280)
at java.awt.Window.dispatchEventImpl(Window.java:2746)
at java.awt.Component.dispatchEvent(Component.java:4711)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:758)
at java.awt.EventQueue.access$500(EventQueue.java:97)
at java.awt.EventQueue$3.run(EventQueue.java:709)
at java.awt.EventQueue$3.run(EventQueue.java:703)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:76)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
at java.awt.EventQueue$4.run(EventQueue.java:731)
at java.awt.EventQueue$4.run(EventQueue.java:729)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:76)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:728)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

Ответы [ 2 ]

0 голосов
/ 06 июня 2018

Нарушения контракта

Я вижу, как нарушаются две части контракта:

  • WrappedBigDecimal.compareTo(null)

Javadoc для Comparable говорит, что compareTo(null) всегда должен бросать NullPointerException.Ваш метод не подчиняется этому.

  • WrappedNumberSorter.getComparator() обработка WrappedBigDecimal(null)

Рассмотрим этот код:

WrappedBigDecimal x = new WrappedBigDecimal(null);
WrappedBigDecimal y = new WrappedBigDecimal(null);

..вместе с этой цитатой из javadoc для Comparator:

Разработчик должен убедиться, что sgn(compare(x, y)) == -sgn(compare(y, x)) для всех x и y.

Используя ваш код (при условии ascending == true), мы имеем compare(x, y) == 1.Но compare(y, x) == 1.Начиная с sgn(1) != -sgn(1), это является нарушением контракта интерфейса Comparator.

Общие комментарии

Ваша общая цель сортировки значений NULL в начале или в конце хорошая, на самом деле этоочень распространенный.Но вам не хватает многих возможностей использовать библиотечный код.

Вспомогательные методы Comparator

В Java почти всегда проще использовать статические вспомогательные методы в классе Comparator, чемнапишите свои собственные compare() методы вручную.Действительно, для вашего случая есть индивидуальный вариант: nullsLast().

Необязательно

Вместо написания вашей собственной оболочкикласс для работы с null значениями для BigDecimal, просто используйте Optional<BigDecimal>.Это именно то, для чего он предназначен.

Полное решение

Поскольку Optional<T> является очень общим контейнерным классом, он не может сам реализовать Comparable<Optional<T>> (нет гарантии, чтотипа это упаковка Comparable).Но статические вспомогательные методы Comparator упрощают выполнение самих себя.

Сначала мы сортируем по наличию значения или нет, следя за тем, чтобы пустые опционы шли не в начало, а в конец.

Затем, когда у нас есть пара опций, чье «присутствие» эквивалентно (либо присутствует, либо оба отсутствуют), мы даем простую лямбду для извлечения значения для сравнения.orElse() - это как раз то, что нам нужно: если значение присутствует, возьмите его и сравните;если значение отсутствует, предоставьте запасной вариант.(Помните, что последний случай имеет место только тогда, когда оба сравниваемых необязательных опциона пусты, поэтому не имеет значения какое значение мы используем в качестве запасного, оно всегда просто сравнивается с самим собой, даваярезультат, который мы хотим: два пустых опциональных эквивалента.)

Comparator.<Optional<BigDecimal>,Boolean>comparing(Optional::isPresent)
          .reversed() //default ordering of booleans is false before true
          .thenComparing(o -> o.orElse(BigDecimal.ONE))
0 голосов
/ 06 июня 2018

Можно, избавиться от класса-обёртки.А в методе сортировки проверьте значения каждого объекта и отсортируйте их соответственно.Например, вы хотите, чтобы все нулевые экземпляры были помещены в нижнюю часть.Если один или оба значения равны нулю, обрабатывайте их как значения -1

т.е.)

@Override
public Comparator<?> getComparator(final int column) {
    return new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            if (o1 == null || o2 == null)
                return -1;
            /**
             * Do the rest of your checks here
             */
            return o1.compareTo(o2);
        }
    };
}

Кроме того, похоже, что вы ожидаете сравнения объектов "не-BigDecimal"в вашем BigDecimal сопоставимом объекте.

A) Если BigDecimal уже реализует Comparable, нет необходимости повторно реализовывать интерфейс.Просто переопределите существующий метод из суперкласса.

B) WrappedBigDecimal - это универсальный тип, ваш код, кажется, принимает другие типы, которые не относятся к этому типу, что может быть не важно здесь, но может определенно вызыватьваши ошибки

...