Компаратор подходит для TreeSet, когда нет отличительного поля

Предположим, у меня есть класс, не реализующий интерфейс Comparable, например

class Dummy {
}

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

Collection<Dummy> col = new ArrayList<>();
Map<Dummy, Integer> map = new HashMap<>();
for (int i = 0; i < 12; i++) {
    Dummy d = new Dummy();
    col.add(d);
    map.put(d, i % 4);
}

Теперь я хочу отсортировать эту коллекцию, используя класс TreeSet с настраиваемым компаратором:

TreeSet<Dummy> sorted = new TreeSet<>(new Comparator<Dummy>() {
    @Override
    public int compare(Dummy o1, Dummy o2) {
        return map.get(o1) - map.get(o2);
    }
});
sorted.addAll(col);

Результат явно неудовлетворительный (содержит меньше элементов, чем исходная коллекция). Это связано с тем, что такой компаратор несовместим с equals, т.е. иногда возвращает 0 для неравных элементов. Следующей моей попыткой было изменить compare метод компаратора на

@Override
public int compare(Dummy o1, Dummy o2) {
    int d = map.get(o1) - map.get(o2);
    if (d != 0)
        return d;
    if (o1.equals(o2))
        return 0;
    return 1; // is this acceptable?
}

Казалось бы, это дает желаемый результат для этого простого демонстрационного примера, но я все еще сомневаюсь: правильно ли всегда возвращать 1 для неравных (но неотличимых на карте) объектов? Такое отношение по-прежнему нарушает общий контакт для Comparator.compare() метода, потому что sgn(compare(x, y)) == -sgn(compare(y, x)), как правило, неверен. Мне действительно нужно реализовать правильный общий заказ, чтобы TreeSet работал правильно, или вышеперечисленного достаточно? Как это сделать, если у экземпляра нет полей для сравнения?

В качестве более реального примера представьте, что вместо Dummy у вас есть параметр типа T некоторого универсального класса. T может иметь несколько полей и реализовывать через них метод equals(), но вы не знаете эти поля, и все же вам нужно отсортировать экземпляры этого класса в соответствии с некоторой внешней функцией. Возможно ли это с помощью TreeSet?

Редактировать

Использование System.identityHashCode() - отличная идея, но есть (не такая уж маленькая) вероятность столкновения.

Помимо возможности такого столкновения есть еще одна подводная лодка. Предположим, у вас есть 3 объекта: a, b, c такие, что map.get(a) = map.get(b) = map.get(c) (здесь = не присвоение, а математическое равенство), identityHashCode(a) < identityHashCode(b) < identityHashCode(c), a.equals(c) истинно, но a.equals(b) (и, следовательно, c.equals(b)) ложно. После добавления этих 3 элементов в TreeSet в следующем порядке: a, b, c вы можете попасть в ситуацию, когда все они были добавлены в набор, что противоречит предписанному поведению интерфейса Set - он не должен содержать одинаковых элементов. Как с этим бороться?

Кроме того, было бы здорово, если бы кто-нибудь, знакомый с TreeSet механикой, объяснил мне, что означает термин "четко определенный" во фразе "Поведение набора четко определено, даже если его порядок несовместим с равно " из TreeSet javadoc означает.


person John McClane    schedule 11.12.2018    source источник
comment
TreeSet работает, сохраняя элементы в строгом порядке. Если вы не скажете ему, как упорядочить элементы, как вы можете ожидать, что он сможет их найти? Сможете ли вы найти книгу в библиотеке, где предметы просто случайным образом расставлены по полкам?   -  person Dawood ibn Kareem    schedule 12.12.2018
comment
@DawoodibnKareem Достаточно ли вышеуказанного компаратора, если единственное, что мне нужно от TreeSet, - это сортировать элементы во время его создания и выводить результат в виде списка? Мне не нужны расширенные возможности вроде tailSet().   -  person John McClane    schedule 12.12.2018
comment
Нет, на самом деле это не так. Если ваш компаратор не работает должным образом, ваша карта не будет работать должным образом.   -  person Dawood ibn Kareem    schedule 12.12.2018
comment
Пример, который вы приводите для компаратора, кажется довольно произвольным. Если это действительно произвольно, вам может быть лучше использовать LinkedHashSet (чтобы порядок вставок сохранялся для целей итерации)   -  person Roy Shahaf    schedule 16.12.2018
comment
Приведите более конкретный пример того, чего вы пытаетесь достичь   -  person Roy Shahaf    schedule 16.12.2018
comment
@RoyShahaf Вопрос конкретно о TreeSet. Он должен сортировать элементы в соответствии с некоторой заданной функцией (считайте ее произвольной ToIntFunction вместо map.get() в примере). Порядок вставки (который использует LinkedHashSet) может не соответствовать результатам этой функции.   -  person John McClane    schedule 16.12.2018
comment
Затем вы должны изменить Dummy так, чтобы equals и hashCode использовали ToIntFunction (если compare (dummy1, dummy2) == 0, equals должен возвращать true, а hashCode должен быть таким же)   -  person Roy Shahaf    schedule 16.12.2018
comment
@RoyShahaf Нет, Dummy - это черный ящик в этом примере, я не могу его изменить (см. Параграф о параметре T универсального класса).   -  person John McClane    schedule 16.12.2018
comment
вы можете изменить карту? если каждый манекен сопоставлен с парой, состоящей из int и uuid, вы можете гарантировать уникальность более надежным способом   -  person Roy Shahaf    schedule 16.12.2018
comment
@RoyShahaf Нет, это невозможно. map просто реализует ToIntFunction для примера, это тоже черный ящик. Единственное, что я могу изменить, это компаратор, который я использую при создании TreeSet. Посмотрите на пример решения, который дал JB Nizet.   -  person John McClane    schedule 16.12.2018
comment
Что ж, я бы рекомендовал взглянуть на stackoverflow.com/questions/909843/, тогда.   -  person Roy Shahaf    schedule 16.12.2018
comment
Чтобы различать логически равные, но разные объекты в TreeSet, я бы предложил использовать Guava Ordering.arbitrary ().   -  person Stuart Marks    schedule 18.12.2018
comment
Что касается вашего абзаца, еще одна ошибка - это следствие несовместимости компаратора с равными. То есть у вас может быть два элемента a и b в SortedSet, так что compare (a, b)! = 0, но a.equals (b) истинно. Таким образом, как вы заметили, этот набор нарушает инвариант Set, что нет двух элементов, таких, что a.equals (b) истинно. SortedSet заменяет это инвариантом, что нет двух элементов, таких, что compare (a, b) == 0. (Вы должны читать между строк документации, чтобы понять это.)   -  person Stuart Marks    schedule 18.12.2018
comment
Вы уверены, что сценарий, который вы предлагаете в своей редакции, возможен? Документация для Object.hashCode требует, чтобы два объекта, эквивалентных через Object.equals, также возвращали одно и то же целое число из Object.hashCode. System.identityHashCode Возвращает тот же хэш-код для данного объекта, который был бы возвращен методом hashCode () по умолчанию, независимо от того, переопределяет ли класс данного объекта hashCode (). Итак, a.equals (c) == true подразумевает System.identityHashCode (a) == System.identityHashCode (c). и поэтому identityHashCode (a) ‹identityHashCode (b)‹ identityHashCode (c) не может быть истинным   -  person heisbrandon    schedule 19.12.2018
comment
@heisbrandon Итак, a.equals (c) == true подразумевает System.identityHashCode (a) == System.identityHashCode (c). Нет, a.equals(c) == true подразумевает только a.hashCode() == c.hashCode(). Так что сценарий вполне возможен, когда hashCode() был переопределен (и таким образом отличается от System.identityHashCode()).   -  person John McClane    schedule 19.12.2018
comment
@JohnMcClane, если вы не переопределили равное, тогда System.identityHashCode вернет одно и то же значение для равных объектов независимо от реализации hashCode.   -  person heisbrandon    schedule 19.12.2018
comment
@heisbrandon Я не переопределял equals и hashCode в классе Dummy только для простоты. Я все еще ищу более или менее общее решение (см. Параграф Для более реального примера представьте, что ...). Вы можете предположить, что hashCode согласуется с equals (независимо от того, переопределяются ли оба или нет), как в общем контракт hashCode в JavaDoc.   -  person John McClane    schedule 19.12.2018
comment
Извините, @JohnMcClane, я пропустил эту часть вашего вопроса, но я полагаю, что было бы разумнее предположить, что это так. Есть ли причина, по которой TreeSet абсолютно необходим? Похоже, вы могли бы использовать ArrayList и вызвать Collections.sort (список ‹T›, Comparator ‹? Super T› c).   -  person heisbrandon    schedule 19.12.2018
comment
@heisbrandon Я имею в виду, что вы можете положиться на общий договор hashCode, отвечая на этот вопрос. Я придерживаюсь TreeSet, потому что он сочетает в себе возможности сортировки с возможностями набора (те, которые не могут предоставить списки). Кроме того, мне нужно дополнительное объяснение TreeSet генерального контракта (я добавил параграф к вопросу, чтобы прояснить это).   -  person John McClane    schedule 19.12.2018
comment
@StuartMarks Извините за поздний ответ. Я посмотрел на Ordering.arbitrary() Guava и нашел в его документации javadoc фразу, которая недвусмысленно предостерегает от ее использования в SortedSet. В частности, может быть случай, когда я добавляю в набор два равных (по equals()), но не идентично равных (по x == y) объекта, и я ожидаю, что второй будет отклонен от добавления. Другими словами, я ожидаю, что компаратор будет согласован с equals() (который указан, а также hashCode(), согласованный с ним, может использоваться в вашей реализации, но не может быть изменен).   -  person John McClane    schedule 19.12.2018
comment
@JohnMcClane Правильно, фраза, которая предостерегает от использования Ordering.arbitrary для SortedSet, - это то же самое предостережение об использовании компаратора, несовместимого с equals. По сути SortedSet изменяет семантику членства в множестве, чтобы следовать за компаратором вместо метода equals (). Это нормально, если ваше приложение может с этим справиться. Но если вы смешиваете обычные наборы и SortedSets, вы можете получить неожиданное поведение, в зависимости от того, чей тест на членство в наборе используется. Думаю, мне следует ввести полный ответ ....   -  person Stuart Marks    schedule 20.12.2018
comment
@StuartMarks Я думаю, это не меняет семантику. TreeSet по-прежнему Set, но для своей работы использует компаратор. Вот почему в javadoc было сказано, что компаратор должен соответствовать equals(). Я понимаю, что если это не так, то TreeSet не Set, но все же любопытно, можно ли его использовать только для сортировки неравных элементов с таким «неполным» компаратором. Это была вторая часть моего вопроса. Первым (и более важным) был запрос на «хороший» (т.е. соответствующий equals()) компаратор в обстоятельствах, когда нет доступных полей.   -  person John McClane    schedule 20.12.2018
comment
Под семантикой я имею в виду, как разные наборы определяют, являются ли элементы дубликатами. Set считает элементы повторяющимися, когда equals возвращает истину. SortedSet считает элементы дублированными, когда compare возвращает 0. Как правило, они имеют разную семантику. SortedSet может иметь ту же семантику, что и Set, если его метод сравнения совместим с equals. Но SortedSet необязательно иметь ту же семантику, что и Set, чтобы вести себя с пользой; это тот случай, если его метод сравнения несовместим с равенством.   -  person Stuart Marks    schedule 20.12.2018
comment
Я начал с ответа на этот вопрос, но меня смутило множество правок и ваши объяснения вопросов в этой серии комментариев. Возможно, вам стоит задать один или несколько новых вопросов. Можно сосредоточиться на реальной проблеме, которую вы пытаетесь решить. Другой может сосредоточиться на тонкостях компараторов и согласованности с равенствами для SortedSets. Объединение всего этого в один вопрос действительно сбивает с толку.   -  person Stuart Marks    schedule 20.12.2018
comment
Я все равно добавил ответ, но, возможно, это не тот, который вы ищете.   -  person Stuart Marks    schedule 20.12.2018
comment
Один простой вариант - использовать одну из других сортируемых коллекций, которая поддерживает несколько одинаковых элементов. Например, список легко отсортировать с помощью Collections.sort (), а затем элементы будут упорядочены, как указано, с одинаковыми элементами рядом друг с другом в произвольном порядке.   -  person leorleor    schedule 23.12.2018


Ответы (3)


Если у вас нет абсолютно большого количества фиктивных объектов и действительно не повезло, вы можете использовать System.identityHashCode() для разрыва связей:

Comparator.<Dummy>comparingInt(d -> map.get(d))
          .thenComparingInt(System::identityHashCode)

Ваш компаратор неприемлем, поскольку он нарушает контракт: у вас есть d1> d2 и d2> d1 одновременно, если они не равны и не имеют одинакового значения на карте.

person JB Nizet    schedule 11.12.2018
comment
Спасибо @John, исправлено. - person JB Nizet; 12.12.2018
comment
Это шаг в правильном направлении, но я бы с осторожностью полагался на уникальность identityHashCode. Это определенно не уникально. После создания около 100 000 объектов можно получить повторяющиеся значения. - person Stuart Marks; 15.12.2018
comment
@StuartMarks Это очень важное замечание, спасибо. Я попытался создать кучу Dummy объектов, добавляя их в список (чтобы избежать сбора мусора) и одновременно добавляя их identityHashCode в набор. И уже 105842 Dummy у меня есть повторяющийся hashCode. Итак, этот ответ необходимо улучшить. - person John McClane; 16.12.2018
comment
Я отредактировал вопрос, посмотрите добавленный абзац. - person John McClane; 16.12.2018
comment
Если вам нужно справиться со случаем, когда у вас есть коллизия в System::identityHashCode, вам нужно будет произвольно разорвать эти связи (например, по порядку, в котором элементы появляются во входной коллекции) и запомнить эти решения. Что-то вроде 1) сопоставьте входную коллекцию со списком, используя вышеуказанный Comparator, затем 2) создайте новый компаратор для TreeSet на основе List.indexOf из этого списка (или вычислите карту для большей эффективности). - person Rich; 20.12.2018

Этот ответ охватывает только первый пример вопроса. На оставшуюся часть вопроса и различные правки, я думаю, лучше ответить в виде отдельных конкретных вопросов.

Первый пример устанавливает 12 экземпляров Dummy, создает карту, которая сопоставляет каждый экземпляр с Integer в диапазоне [0, 3], а затем добавляет 12 экземпляров Dummy к TreeSet. Этот TreeSet снабжен компаратором, который использует карту Dummy-to-Integer. В результате TreeSet содержит только четыре экземпляра Dummy. Пример завершается следующим утверждением:

Результат явно неудовлетворительный (содержит меньше элементов, чем исходная коллекция). Это связано с тем, что такой компаратор несовместим с равенством, т.е. иногда возвращает 0 для неравных элементов.

Последнее предложение неверно. Результат содержит меньше элементов, чем было вставлено, потому что компаратор считает многие экземпляры дубликатами, и поэтому они не вставляются в набор. Метод equals вообще не обсуждается. Следовательно, концепция согласованности с равными не имеет отношения к этому обсуждению. TreeSet никогда не звонит equals. Компаратор - единственное, что определяет принадлежность к TreeSet.

Это кажется неудовлетворительным результатом, но только потому, что мы знаем, что существует 12 различных Dummy экземпляров. Однако TreeSet не знает, что они различны. Он знает только, как сравнивать Dummy экземпляры с помощью компаратора. Когда он это делает, он обнаруживает, что некоторые из них являются дубликатами. То есть компаратор иногда возвращает 0, даже если он вызывается с Dummy экземплярами, которые мы считаем разными. Вот почему только четыре Dummy экземпляра попадают в TreeSet.

Я не совсем уверен, каков желаемый результат, но похоже, что результат TreeSet должен содержать все 12 экземпляров, упорядоченных по значениям на карте фиктивного преобразования в целое число. Мое предложение состояло в том, чтобы использовать Guava _ 19_, который предоставляет компаратор, который различает отдельные, но в остальном равные элементы, но делает это способом, который удовлетворяет общему соглашению Comparator. Если вы создадите TreeSet следующим образом:

SortedSet<Dummy> sorted = new TreeSet<>(Comparator.<Dummy>comparingInt(map::get)
                                                  .thenComparing(Ordering.arbitrary()));

в результате TreeSet содержит все 12 Dummy экземпляров, отсортированных по Integer значению на карте, и с Dummy экземплярами, которые сопоставляются с одним и тем же значением, упорядоченным произвольно.

В комментариях вы заявили, что Ordering.arbitrary документ недвусмысленно предостерегает от его использования в SortedSet. Это не совсем так; этот документ говорит,

Поскольку упорядочение основано на идентичности, оно не согласуется с Object.equals (Object), как определено Comparator. Будьте осторожны при построении SortedSet или SortedMap из него, поскольку результирующая коллекция не будет вести себя в точном соответствии со спецификацией.

Фраза не в точности соответствует спецификации означает, что она будет вести себя странно, как описано в документе класса _ 28_:

Упорядочение, наложенное компаратором c на набор элементов S, называется согласованным с equals тогда и только тогда, когда c.compare(e1, e2)==0 имеет то же логическое значение, что и e1.equals(e2) для каждого e1 и e2 в S.

Следует проявлять осторожность при использовании компаратора, способного устанавливать порядок, несовместимый с равенством, для упорядочивания отсортированного набора (или отсортированной карты). Предположим, что отсортированный набор (или отсортированная карта) с явным компаратором c используется с элементами (или ключами), взятыми из набора S. Если порядок, наложенный c на S, несовместим с equals, отсортированный набор (или отсортированная карта) будет вести себя странно. В частности, отсортированный набор (или отсортированная карта) нарушит общий договор для набора (или карты), который определен в терминах equals.

Например, предположим, что кто-то добавляет два элемента a и b так, что (a.equals(b) && c.compare(a, b) != 0) к пустому TreeSet с компаратором c. Вторая операция добавления вернет true (и размер набора деревьев увеличится), потому что a и b не эквивалентны с точки зрения набора деревьев, даже если это противоречит спецификации метода Set.add.

Вы, кажется, указали, что это странное поведение недопустимо, поскольку Dummy элементы, которые equals не должны появляться в TreeSet. Но класс Dummy не отменяет equals, поэтому кажется, что здесь скрывается дополнительное требование.

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

ОБНОВЛЕНИЕ 22 декабря 2018 г.

Перечитав правки вопросов и комментарии, я думаю, что наконец понял, что вы ищете. Вам нужен компаратор для любого объекта, который обеспечивает первичное упорядочение на основе некоторой функции с целым значением, которая может приводить к дублированию значений для неравных объектов (как определено методом equals объектов). Следовательно, требуется вторичный порядок, который обеспечивает полное упорядочение по всем неравным объектам, но возвращает ноль для объектов, которые являются equals. Это означает, что компаратор должен быть совместим с равными.

Ordering.arbitrary Guava приближается к нему в том смысле, что он обеспечивает произвольное полное упорядочение по любым объектам, но возвращает ноль только для идентичных объектов (то есть ==), но не для объектов, которые являются equals. Таким образом, это несовместимо с равными.

В таком случае кажется, что вам нужен компаратор, обеспечивающий произвольный порядок неравных объектов. Вот функция, которая его создает:

static Comparator<Object> arbitraryUnequal() {
    Map<Object, Integer> map = new HashMap<>();
    return (o1, o2) -> Integer.compare(map.computeIfAbsent(o1, x -> map.size()),
                                       map.computeIfAbsent(o2, x -> map.size()));
}

По сути, это присваивает порядковый номер каждому вновь обнаруженному неравному объекту и сохраняет эти номера на карте, хранимой компаратором. В качестве счетчика он использует размер карты. Поскольку объекты никогда не удаляются с этой карты, размер и, следовательно, порядковый номер всегда увеличивается.

(Если вы намереваетесь использовать этот компаратор одновременно, например, при параллельной сортировке, HashMap следует заменить на ConcurrentHashMap, а трюк с размером следует изменить, чтобы использовать AtomicInteger, который увеличивается при добавлении новых записей.)

Обратите внимание, что карта в этом компараторе создает записи для каждого неравного объекта, который он когда-либо видел. Если это присоединено к TreeSet, объекты будут сохраняться на карте компаратора даже после того, как они были удалены из TreeSet. Это необходимо для того, чтобы при добавлении или удалении объектов они сохранили постоянный порядок с течением времени. Ordering.arbitrary Guava использует слабые ссылки, позволяющие собирать объекты, если они больше не используются. Мы не можем этого сделать, потому что нам нужно сохранить порядок неидентичных, но равных объектов.

Вы бы использовали это так:

SortedSet<Dummy> sorted = new TreeSet<>(Comparator.<Dummy>comparingInt(map::get)
                                                  .thenComparing(arbitraryUnequal()));

Вы также спросили, что означает следующее:

Поведение набора четко определено, даже если его порядок несовместим с равенством

Предположим, вы использовали TreeSet, используя компаратор, несовместимый с равными, например, тот, который использует Ordering.arbitrary Гуавы, показанный выше. TreeSet по-прежнему будет работать, как ожидалось, в соответствии с самим собой. То есть он будет поддерживать объекты в полном порядке, он не будет содержать никаких двух объектов, для которых компаратор возвращает ноль, и все его методы будут работать, как указано. Однако может существовать объект, для которого contains возвращает истину (поскольку это вычислено с использованием компаратора), но для которого equals ложно, если вызывается с объектом, фактически находящимся в наборе.

Например, BigDecimal это Comparable, но его метод сравнения несовместим с равенством:

> BigDecimal z = new BigDecimal("0.0")
> BigDecimal zz = new BigDecimal("0.00")
> z.compareTo(zz)
0
> z.equals(zz)
false
> TreeSet<BigDecimal> ts = new TreeSet<>()
> ts.add(z)
> HashSet<BigDecimal> hs = new HashSet<>(ts)
> hs.equals(ts)
true
> ts.contains(zz)
true
> hs.contains(zz)
false

Это то, что имеет в виду спецификация, когда говорится, что вещи могут вести себя странно. У нас есть два равных набора. Тем не менее, они сообщают о разных результатах для contains одного и того же объекта, а TreeSet сообщает, что он содержит объект, даже если этот объект не равен объекту в наборе.

person Stuart Marks    schedule 20.12.2018
comment
Спасибо за ответ, он решает проблему столкновения с ответом JB Nizet, но все еще неполный. Обратите внимание на слово like в самом начале вопроса. Вы действительно думаете, что мне пришлось отсортировать 12 экземпляров такого простого класса и обеспокоить сообщество таким простым вопросом, выбрав причину, по которой Вопрос широко применим к большой аудитории за награду? Параграф Для более реального примера представьте, что ... существует с самого начала. Он явно (не скрываясь) оговаривает возможность переопределенного метода equals(). - person John McClane; 20.12.2018
comment
Требование к компаратору простое: он должен соответствовать equals() (независимо от того, переопределяется последний или нет) и различать элементы, которые исходный компаратор (map::get в вашем примере) не различает. - person John McClane; 20.12.2018
comment
@JohnMcClane Конечно, класс Dummy - это пример кода; все (включая меня) это понимают. Сложно то, что вы просите компаратор, совместимый с equals, но вы не описали, что этот equals метод может сделать. Кажется, что equals может смотреть на некоторые поля, не известные компаратору, но вы требуете, чтобы компаратор различал объекты (и предоставлял общий порядок) на основе этих неизвестных полей. Мне это кажется невозможным. Или, может быть, я все еще что-то не понимаю. - person Stuart Marks; 20.12.2018
comment
Я не показываю поля тех классов, экземпляры которых должны быть отсортированы не из-за секретности этих полей, а из-за того, что у меня их нет! Я пытаюсь отсортировать экземпляры типа (T) универсального класса. Описанная вами проблема является общей для общих классов: у вас есть GenericClass<T extends Bound> и вы пытаетесь максимально использовать его, но единственные поля и методы, которые вы можете использовать при обращении к T, - это те, которые присутствуют в Bound. В моем случае Bound = Object, поэтому в вашем распоряжении только методы из Object. - person John McClane; 20.12.2018
comment
Но это немало. У вас есть сам метод equals(). И не забывайте о hashCode(), который (в отличие от несчастного identityHashCode()), как вы можете предположить, соответствует equals(). - person John McClane; 20.12.2018
comment
@JohnMcClane Хорошо, я думаю, мы добились небольшого прогресса. Вы пытаетесь отсортировать некоторые объекты по некоторой классификации первого уровня, которая в вашем примере воплощена в карте Dummy-to-Integer. На этом первом уровне сортировки будет куча неравных объектов, которые все имеют одинаковое значение, поэтому очевидно, что нужны дополнительные критерии. Вам важны эти критерии, или они могут быть произвольными? (За исключением того, что критерии соответствуют равным.) - person Stuart Marks; 20.12.2018
comment
@JohnMcClane Мне также приходит в голову, что мы зациклены на SortedSet, который требует хорошего компаратора. Сработает ли это, чтобы сохранить элементы в списке, а затем просто отсортировать их в соответствии с классификацией первого уровня? Сортировка стабильна, поэтому относительный порядок среди эквивалентных объектов первого уровня будет сохранен. - person Stuart Marks; 20.12.2018
comment
Я понимаю, что если бы мне пришлось сохранить порядок вставки, то, вероятно, лучшим решением было бы: 1. Добавить все элементы в LinkedHashSet, чтобы удалить дубликаты; 2. Запишите их все в List; 3. Отсортируйте результаты с помощью частичного компаратора (map::get). Но мне нужен именно SortedSet как результат, и суть вопроса заключается в том, чтобы создать как можно меньше накладных расходов. Помните, что каждый дополнительный набор примерно в 10 раз занимает память, как массив, содержащий ту же коллекцию. Что касается дополнительных требований, то они (в указанном порядке): 1. Производительность; 2. Меньше накладных расходов на память. - person John McClane; 21.12.2018
comment
Почему вы используете Integer.compare в arbitraryUnequal() вместо простой разницы? В вашем случае это кажется излишним, потому что map.size() никогда не бывает отрицательным. - person John McClane; 24.12.2018
comment
@JohnMcClane В основном привычка. Integer.compare никогда не бывает неправильным. Иногда вычитание неверно, и его правильность должна быть подтверждена в зависимости от контекста. Контекст может измениться в процессе обслуживания, что приведет к возникновению риска ошибок. - person Stuart Marks; 24.12.2018
comment
Спасибо за дополнение к вашему ответу, я узнал из него много полезного, хотя я рекомендую прибегать к разрешению привязки на основе карты только на последнем этапе, как я сделал в своем ответе, потому что ведение карты, охватывающей все элементы набор может потреблять память, и получение его значений может занять много времени по сравнению с hashCode(). - person John McClane; 24.12.2018
comment
@JohnMcClane Отлично, рад помочь. И да, промежуточное упорядочение на основе hashCode кажется полезной оптимизацией. Спасибо за награду! - person Stuart Marks; 25.12.2018

Вот компаратор, который у меня получился. Это одновременно надежно и эффективно с точки зрения памяти.

public static <T> Comparator<T> uniqualizer() {
    return new Comparator<T>() {
        private final Map<T, Integer> extraId = new HashMap<>();
        private int id;

        @Override
        public int compare(T o1, T o2) {
            int d = Integer.compare(o1.hashCode(), o2.hashCode());
            if (d != 0)
                return d;
            if (o1.equals(o2))
                return 0;
            d = extraId.computeIfAbsent(o1, key -> id++)
              - extraId.computeIfAbsent(o2, key -> id++);
            assert id > 0 : "ID overflow";
            assert d != 0 : "Concurrent modification";
            return d;
        }
    };
}

Он создает полное упорядочение для всех объектов данного класса T и, таким образом, позволяет различать объекты, не различимые данным компаратором, путем присоединения к нему следующим образом:

Comparator<T> partial = ...
Comparator<T> total = partial.thenComparing(uniqualizer());

В примере, приведенном в вопросе, T равно Dummy и

partial = Comparator.<Dummy>comparingInt(map::get);

Обратите внимание, что вам не нужно указывать тип T при вызове uniqualizer(), компилятор автоматически определяет его через вывод типа. Вам нужно только убедиться, что hashCode() в T соответствует equals(), как описано в генеральный договор hashCode(). Затем uniqualizer() предоставит вам компаратор (total), соответствующий equals(), и вы можете использовать его в любом коде, который требует сравнения объектов типа T, например. при создании TreeSet:

TreeSet<T> sorted = new TreeSet<>(total);

или сортировка списка:

List<T> list = ...
Collections.sort(list, total);
person John McClane    schedule 24.12.2018
comment
Чего вы этим добиваетесь? Какую проблему вы решаете? - person Thorbjørn Ravn Andersen; 24.12.2018
comment
@ ThorbjørnRavnAndersen Мне пришлось создать SortedSet объектов общего типа, и начального компаратора (который я намеревался использовать в качестве параметра для конструктора TreeSet) для этой цели было недостаточно. - person John McClane; 24.12.2018
comment
Какой смысл сортировать универсальный тип? Я пытаюсь понять, почему вы были вынуждены создавать ключ сортировки на лету. - person Thorbjørn Ravn Andersen; 24.12.2018
comment
@ ThorbjørnRavnAndersen Это было сделано для того, чтобы TreeSet работал как обычный набор, т.е. чтобы принимать отдельные (по equals()) и отбрасывать повторяющиеся элементы, чтобы давать ожидаемые результаты contains() и т. Д., Одновременно используя возможности сортировки. Исходные критерии сортировки подходили для списка, но не для набора. Ключевым моментом здесь является то, что критерий сортировки был внешним, то есть не определялся самими полями входных объектов. - person John McClane; 24.12.2018