Возможно ли в Java сделать что-то вроде Comparator, но для реализации пользовательских equals () и hashCode () - PullRequest
49 голосов
/ 05 марта 2011

У меня есть массив объектов, и я хочу объединить его с другим массивом объектов, за исключением объектов с одинаковыми идентификаторами. Эти объекты используются во многих местах системы и не имеют хеш-кода или равнозначных реализаций. Поэтому я не хочу реализовывать hashCode() и equals(), потому что боюсь что-то сломать где-нибудь в системе, где используются эти объекты, и я не знаю об этом.

Я хочу поместить все эти объекты в набор, но каким-то образом заставить объекты использовать пользовательские hashCode() и equals(). Что-то вроде кастомного Comparator, но для равных.

Ответы [ 7 ]

34 голосов
/ 05 марта 2011

Да, это возможно. Но это не позволит вам помещать ваши объекты в HashMap, HashSet и т. Д. Это потому, что стандартные классы коллекции ожидают, что ключевые объекты предоставляют методы equals и hashCode. (Вот как они предназначены для работы ...)

Альтернативы:

  1. Реализация класса-обертки, который содержит экземпляр реального класса и обеспечивает собственную реализацию equals и hashCode.

  2. Реализуйте свои собственные классы на основе хеш-таблиц, которые могут использовать «хешируемый» объект для обеспечения функций равенства и хэш-кода.

  3. Укуси пулю и осуществи переопределения equals и hashCode для соответствующих классов.

На самом деле, третий вариант, вероятно, самый лучший, потому что вашей кодовой базе, скорее всего, требуется , чтобы использовать согласованное представление о том, что означает, что эти объекты равны. Есть и другие вещи, которые предполагают, что ваш код нуждается в пересмотре. Например, тот факт, что в настоящее время он использует массив объектов вместо реализации Set для представления того, что, по-видимому, должно быть множеством.

С другой стороны, возможно, была / есть какая-то реальная (или воображаемая) причина производительности для текущей реализации; например сокращение использования памяти. В этом случае вам, вероятно, следует написать несколько вспомогательных методов для выполнения таких операций, как объединение двух наборов, представленных в виде массивов.

13 голосов
/ 06 марта 2011

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

10 голосов
/ 23 июня 2014

HashingStrategy - это концепция, которую вы ищете. Это интерфейс стратегии, который позволяет вам определять пользовательские реализации equals и hashcode.

public interface HashingStrategy<E>
{
    int computeHashCode(E object);
    boolean equals(E object1, E object2);
}

Как уже отмечали другие, вы не можете использовать HashingStrategy со встроенным HashSet или HashMap. Eclipse Collections включает набор с именем UnifiedSetWithHashingStrategy и карту с именем UnifiedMapWithHashingStrategy.

Давайте посмотрим на пример. Вот простой Data класс, который мы можем использовать в UnifiedSetWithHashingStrategy.

public class Data
{
    private final int id;

    public Data(int id)
    {
        this.id = id;
    }

    public int getId()
    {
        return id;
    }

    // No equals or hashcode
}

Вот как вы можете настроить UnifiedSetWithHashingStrategy и использовать его.

java.util.Set<Data> set =
  new UnifiedSetWithHashingStrategy<>(HashingStrategies.fromFunction(Data::getId));
Assert.assertTrue(set.add(new Data(1)));

// contains returns true even without hashcode and equals
Assert.assertTrue(set.contains(new Data(1)));

// Second call to add() doesn't do anything and returns false
Assert.assertFalse(set.add(new Data(1)));

Почему бы просто не использовать Map? UnifiedSetWithHashingStrategy использует половину памяти UnifiedMap и одну четверть памяти HashMap. А иногда у вас нет удобного ключа и вам нужно создать синтетический ключ, например, кортеж. Это может тратить больше памяти.

Как мы выполняем поиск? Помните, что наборы имеют contains(), но не get(). UnifiedSetWithHashingStrategy реализует Pool в дополнение к MutableSet, поэтому он также реализует форму get().

Примечание: я являюсь коммиттером для Eclipse Collections.

4 голосов
/ 05 марта 2011

Конечно, вы можете создать некоторый внешний объект, обеспечивающий сравнение на равенство и HashCode.Но встроенные коллекции Java не используют такой объект для их сравнения / поиска.

Однажды я создал такой интерфейс в моей коллекции пакетов (только что переведенный на английский):

public interface HashableEquivalenceRelation {

    /**
     * Returns true if two objects are considered equal.
     *
     * This should form an equivalence relation, meaning it
     * should fulfill these properties:
     *  <ul>
     *    <li>Reflexivity:  {@code areEqual(o, o)}
     *            should always return true.</li>
     *    <li>Symmetry: {@code areEqual(o1,o2) == areEqual(o2,o1)}
     *            for all objects o1 and o2</li>
     *    <li>Transitivity: If {@code areEqual(o1, o2)} and {@code areEqual(o2,o3)},
     *            then {@code areEqual(o1,o3}} should hold too.</li>
     *  </ul>
     * Additionally, the relation should be temporary consistent, i.e. the
     * result of this method for the same two objects should not change as
     * long as the objects do not change significantly (the precise meaning of
     * <em>change significantly</em> is dependent on the implementation).
     *
     * Also, if {@code areEqual(o1, o2)} holds true, then {@code hashCode(o1) == hashCode(o2)}
     * must be true too.
     */
    public boolean areEqual(Object o1, Object o2);

    /**
     * Returns a hashCode for an arbitrary object.
     *
     * This should be temporary consistent, i.e. the result for the same
     * objects should not change as long as the object does not change significantly
     * (with change significantly having the same meaning as for {@link areEqual}).
     *
     * Also, if {@code areEqual(o1, o2)} holds true, then {@code hashCode(o1) == hashCode(o2)}
     * must be true too.
     */
    public int hashCode(Object o);

}

Чем у меня была группа интерфейсов CustomCollection, CustomSet, CustomList, CustomMap и т. Д., Определенные как интерфейсы в java.util, но с использованием такого отношения эквивалентности для всех методоввместо встроенного отношения, заданного Object.equals.У меня тоже было несколько реализаций по умолчанию:

/**
 * The equivalence relation induced by Object#equals.
 */
public final static EquivalenceRelation DEFAULT =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2)
        {
            return
                o1 == o2 ||
                o1 != null &&
                o1.equals(o2);
        }
        public int hashCode(Object ob)
        {
            return
                ob == null?
                0 :
                ob.hashCode();
        }
        public String toString() { return "<DEFAULT>"; }
    };

/**
 * The equivalence relation induced by {@code ==}.
 * (The hashCode used is {@link System#identityHashCode}.)
 */
public final static EquivalenceRelation IDENTITY =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2) { return o1 == o2; }
        public int hashCode(Object ob) { return System.identityHashCode(ob); }
        public String toString() { return "<IDENTITY>"; }
    };

/**
 * The all-relation: every object is equivalent to every other one.
 */
public final static EquivalenceRelation ALL =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2) { return true; }
        public int hashCode(Object ob) { return 0; }
        public String toString() { return "<ALL>"; }
    };

/**
 * An equivalence relation partitioning the references
 * in two groups: the null reference and any other reference.
 */
public final static EquivalenceRelation NULL_OR_NOT_NULL =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2)
        {
            return (o1 == null && o2 == null) ||
                (o1 != null && o2 != null);
        }
        public int hashCode(Object o) { return o == null ? 0 : 1; }
        public String toString() { return "<NULL_OR_NOT_NULL>"; }
    };

/**
 * Two objects are equivalent if they are of the same (actual) class.
 */
public final static EquivalenceRelation SAME_CLASS =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2)
        {
            return o1 == o2 || o1 != null && o2 != null &&
                o1.getClass() == o2.getClass();
        }
        public int hashCode(Object o) { return o == null ? 0 : o.getClass().hashCode(); }
        public String toString() { return "<SAME_CLASS>"; }
    };


/**
 * Compares strings ignoring case.
 * Other objects give a {@link ClassCastException}.
 */
public final static EquivalenceRelation STRINGS_IGNORE_CASE =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2)
        {
            return o1 == null ?
                o2 == null :
                ((String)o1).equalsIgnoreCase((String)o2);
        }
        public int hashCode(Object o)
        {
            return o == null ? -12345 : ((String)o).toUpperCase().hashCode();
        }
        public String toString() { return "<STRINGS_IGNORE_CASE>"; }
    };


/**
 * Compares {@link CharSequence} implementations by content.
 * Other object give a {@link ClassCastException}.
 */
public final static EquivalenceRelation CHAR_SEQUENCE_CONTENT =
    new EquivalenceRelation() {
        public boolean areEqual(Object o1, Object o2) 
        {
            CharSequence seq1 = (CharSequence)o1;
            CharSequence seq2 = (CharSequence)o2;
            if (seq1 == null ^ seq2 == null) // nur eins von beiden null
                return false;
            if (seq1 == seq2)   // umfasst auch den Fall null == null
                return true;
            int size = seq1.length();
            if (seq2.length() != size)
                return false;
            for (int i = 0; i < size; i++)
                {
                    if (seq1.charAt(i) != seq2.charAt(i))
                        return false;
                }
            return true;
        }
        /**
         * Entrspricht String.hashCode
         */
        public int hashCode(Object o)
        {
            CharSequence sequence = (CharSequence)o;
            if (sequence == null)
                return 0;
            int hash = 0;
            int size = sequence.length();
            for (int i = 0; i < size; i++)
                {
                    hash = hash * 31 + sequence.charAt(i);
                }
            return hash;
        }
    };
1 голос
/ 25 июня 2016

Просто имел эту проблему и разработал простое решение. Не уверен, насколько она интенсивна; Я уверен, что люди могут уточнить это по линии.

Когда Comparator возвращает 0, элементы совпадают.

public static <E> Set<E> filterSet(Set<E> set, Comparator<E> comparator){
    Set<E> output = new HashSet<E>();
    for(E eIn : set){
        boolean add = true;
        for(E eOut : output){
            if(comparator.compare(eIn, eOut) == 0){
                add = false;
                break;
            }
        }
        if(add) output.add(eIn);
    }
    return output;
}

Мой вариант использования состоял в том, что мне нужно было отфильтровать дублирующиеся URL-адреса, как в URL-адресах, которые указывают на один и тот же документ. У объекта URL есть метод samePage(), который вернет true, если все, кроме фрагмента, одинаково.

filtered = Misc.filterSet(filtered, (a, b) -> a.sameFile(b) ? 0 : 1);
1 голос
/ 05 марта 2011

Поможет ли здесь использование TreeSet ? TreeSet фактически выполняет упорядочивание и поведение на основе Set, используя Compare / CompareTo, и позволяет вам определить собственный компаратор для использования i n одного из конструкторов .

0 голосов
/ 05 марта 2011

Вам не удастся выполнить дедупликацию конкатенации с Comparator.Предположительно, вы хотите сделать что-то вроде этого:

List<Object> list = new ArrayList<Object>();
list.addAll( a );
list.addAll( b );
Collections.sort( list, new MyCustomComparator() );

Проблема в том, что Comparator нужно сравнивать не только для равных / не равных, но и для относительного порядка.Учитывая объекты х и у, которые не равны, вы должны ответить, если один больше, чем другой.Вы не сможете этого сделать, поскольку вы на самом деле не пытаетесь сравнивать объекты.Если вы не дадите последовательного ответа, вы отправите алгоритм сортировки в бесконечный цикл.

У меня есть решение для вас.В Java есть класс LinkedHashSet, преимущество которого заключается в том, что он не позволяет вставлять дубликаты, но поддерживает порядок вставки.Вместо реализации компаратора реализуйте класс-обертку для хранения фактического объекта и реализуйте hashCode / equals.

...