Компаратор подходит для TreeSet, когда нет отличительного поля - PullRequest
0 голосов
/ 12 декабря 2018

Предположим, у меня есть класс, не реализующий интерфейс 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 среднее.

Ответы [ 3 ]

0 голосов
/ 20 декабря 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 экземпляров, упорядоченныхзначения в карте Dummy-to-Integer.Мое предложение состояло в том, чтобы использовать Ordering.arbitrary() в Guava, который предоставляет компаратор, который различает отличные, но в остальном равные элементы, но делает это таким образом, чтобы удовлетворить общий контракт 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 из него, так как результирующая коллекция не будет вести себя точно в соответствии со спецификацией.

Фраза «не ведет себя точно в соответствии со спецификацией» действительно означает, что она будет вести себя «странно»"как описано в документе класса Comparator:

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

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

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

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

В последующих редакциях вопроса есть несколько дополнительных вопросов, но, как я уже упоминал выше, Я думаю, что их лучше рассматривать как отдельный вопрос (ы).

ОБНОВЛЕНИЕ 2018-12-22

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

Guava's Ordering.arbitrary приближается к тому, что он обеспечивает произвольное общее упорядочение по любым объектам, но он возвращает ноль только для объектов, которые идентичны (то есть ==), но не для объектов, которые 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 возвращает значение true (поскольку это вычисляется с использованием компаратора), но для которого equals имеет значение false, если вызывается с объектом, фактически находящимся в наборе.

Например, 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 сообщает, что он содержит объект, даже если этот объект не равен объекту в наборе.

0 голосов
/ 24 декабря 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(), complier автоматически определяет его с помощью вывода типа.Вам нужно только убедиться, что hashCode() в T соответствует equals(), как описано в генеральном контракте из hashCode().Тогда uniqualizer() даст вам компаратор (total), соответствующий equals(), и вы сможете использовать его в любом коде, который требует сравнения объектов типа T, например, при создании TreeSet:

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

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

List<T> list = ...
Collections.sort(list, total);
0 голосов
/ 12 декабря 2018

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

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

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

...