Каковы альтернативы сравнению равенства двух объектов?

StackOverflow https://stackoverflow.com/questions/360265

  •  21-08-2019
  •  | 
  •  

Вопрос

http://leepoint.net/notes-java/data/expressions/22compareobjects.html

Оказывается, определение equals() не является тривиальным;на самом деле, это умеренно сложно сделать правильно, особенно в случае подклассов.Лучший подход к этим проблемам содержится в Core Java Хорстманна, том 1.

Если equals() всегда должна быть переопределена, то каков хороший подход для того, чтобы не быть загнанным в угол необходимостью выполнять сравнение объектов?Каковы некоторые хорошие альтернативы "дизайну"?

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

Я не уверен, что все идет именно так, как я задумывал.Возможно, вопрос должен быть больше похож на "Почему вы хотите сравнить два объекта?" Исходя из вашего ответа на этот вопрос, существует ли альтернативное решение для сравнения?Я не имею в виду другую реализацию equals.Я имею в виду, вообще не использовать равенство.Я думаю, ключевым моментом является то, чтобы начать с этого вопроса: зачем вам сравнивать два объекта.

Это было полезно?

Решение

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

Вы ошибаетесь.Вам следует переопределять equals как можно реже.


Вся эта информация поступает от Эффективная Java, Второе издание (Джош Блох).Глава первого издания, посвященная этому, по-прежнему доступна бесплатно. Скачать.

Из Эффективной Java:

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

Проблема с произвольным переопределением equals/hashCode заключается в наследовании.Некоторые реализации equals рекомендуют тестировать это следующим образом:

if (this.getClass() != other.getClass()) {
    return false; //inequal
}

На самом деле, Затмение (3.4) Редактор Java делает именно это, когда вы генерируете метод с помощью исходных инструментов.По словам Блоха, это ошибка, поскольку нарушает Принцип замещения Лискова.

Из Эффективной Java:

Невозможно расширить создаваемый класс и добавить значение component при сохранении контракта equals .

Два способа минимизации проблем с равенством описаны в Классы и интерфейсы глава:

  1. Предпочтение композиции перед наследованием
  2. Оформлять и оформлять документы для наследования или же запретить это

Насколько я могу видеть, единственной альтернативой является проверка равенства во внешней по отношению к классу форме, и то, как это будет выполнено, будет зависеть от дизайна типа и контекста, в котором вы пытались его использовать.

Например, вы могли бы определить интерфейс, который документирует, как он должен был сравниваться.В приведенном ниже коде экземпляры службы могут быть заменены во время выполнения более новой версией того же класса - в этом случае при использовании разных загрузчиков классов сравнения equals всегда возвращали бы false , поэтому переопределение equals / hashCode было бы избыточным.

public class Services {

    private static Map<String, Service> SERVICES = new HashMap<String, Service>();

    static interface Service {
        /** Services with the same name are considered equivalent */
        public String getName();
    }

    public static synchronized void installService(Service service) {
        SERVICES.put(service.getName(), service);
    }

    public static synchronized Service lookup(String name) {
        return SERVICES.get(name);
    }
}

"Зачем вам понадобилось сравнивать два объекта?"

Очевидный пример - проверить, совпадают ли две строки (или две Файлы, или URIs ( значения )).Например, что, если бы вы хотели создать набор файлов для синтаксического анализа.По определению, набор содержит только уникальные элементы.Java- это Установленный тип полагается на методы equals / hashCode для обеспечения уникальности своих элементов.

Другие советы

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

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

Как насчет того, чтобы просто сделать это правильно?

Вот мой шаблон equals, который представляет собой знания, применяемые из Effective Java Джошем Блохом.Прочтите книгу для получения более подробной информации:

@Override
public boolean equals(Object obj) {
    if(this == obj) {
        return true;
    }

    // only do this if you are a subclass and care about equals of parent
    if(!super.equals(obj)) {
        return false;
    }
    if(obj == null || getClass() != obj.getClass()) {
        return false;
    }
    final YourTypeHere other = (YourTypeHere) obj;
    if(!instanceMember1.equals(other.instanceMember1)) {
       return false;
     }
     ... rest of instanceMembers in same pattern as above....
     return true;
 }

Мм рт . ст.

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

Если необходимы два объекта с одинаковыми входными данными (параметрами создания), фабрика вернет один и тот же экземпляр ref, и тогда будет достаточно использования "==".

Такой подход полезен только при определенных обстоятельствах.И в большинстве случаев это выглядело бы излишеством.

Взгляните на этот ответ знать, как реализовать такую вещь.

внимание, это большой объем кода

Для краткости посмотрите, как работает класс-оболочка начиная с java 1.5

Integer a = Integer.valueOf( 2 );
Integer b = Integer.valueOf( 2 );

a == b 

верно, пока

new Integer( 2 ) == new Integer( 2 )  

это ложь.

Он внутренне сохраняет ссылку и возвращает ее, если входное значение такое же.

Как вы знаете, Integer доступен только для чтения

Нечто подобное происходит с классом String, о котором был задан этот вопрос.

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

  1. Эти два объекта действительно ЯВЛЯЮТСЯ одним И тем же экземпляром.Нет смысла использовать equals , потому что вы можете использовать == .Кроме того, и поправьте меня, если я забыл, как это работает в Java, метод equals по умолчанию делает это, используя автоматически сгенерированные хэш-коды.
  2. Эти два объекта имеют ссылки на одни и те же экземпляры, но не являются одним и тем же экземпляром.Это полезно, э-э, иногда...особенно, если они являются сохраняемыми объектами и ссылаются на один и тот же объект в базе данных.Для этого вам нужно было бы определить свой метод equals.
  3. Два объекта имеют ссылки на объекты, равные по значению, хотя они могут быть или не быть одинаковыми экземплярами (другими словами, вы сравниваете значения по всей иерархии).

Зачем вам понадобилось сравнивать два объекта?Что ж, если они равны, вы хотели бы сделать что-то одно, а если нет, вы хотели бы сделать что-то другое.

Тем не менее, это зависит от конкретного случая.

Основная причина переопределения equals() в большинстве случаев заключается в проверке на наличие дубликатов в определенных коллекциях.Например, если вы хотите использовать Set для хранения созданного вами объекта, вам нужно переопределить equals() и hashCode() внутри вашего объекта.То же самое применимо, если вы хотите использовать свой пользовательский объект в качестве ключа на Карте.

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

Например, если у вас был простой компонент с именем NameBean с единственным строковым атрибутом 'name', вы могли бы создать два экземпляра NameBean (напримерname1 и name2), каждый с одинаковым значением атрибута 'name' (например,"Алиса").Затем вы могли бы добавить как name1, так и name2 к набору, и набор был бы размером 2, а не размером 1, как и предполагалось.Аналогично, если у вас есть карта, такая как Map, для сопоставления компонента name с каким-либо другим объектом, и вы сначала сопоставили name1 со строкой "first", а затем сопоставили name2 со строкой "second", у вас будут обе пары ключ / значение на карте (напримерname1->"первый", name2->"второй").Поэтому, когда вы выполняете поиск по карте, оно возвращает значение, сопоставленное с точной ссылкой, которую вы передаете, которая является либо name1, name2, либо другой ссылкой с именем "Алиса", которая вернет null.

Вот конкретный пример, которому предшествует результат его запуска:

Выходной сигнал:

Adding duplicates to a map (bad):
Result of map.get(bean1):first
Result of map.get(bean2):second
Result of map.get(new NameBean("Alice"): null

Adding duplicates to a map (good):
Result of map.get(bean1):second
Result of map.get(bean2):second
Result of map.get(new ImprovedNameBean("Alice"): second

Код:

// This bean cannot safely be used as a key in a Map
public class NameBean {
    private String name;
    public NameBean() {
    }
    public NameBean(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return name;
    }
}

// This bean can safely be used as a key in a Map
public class ImprovedNameBean extends NameBean {
    public ImprovedNameBean(String name) {
        super(name);
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if(obj == null || getClass() != obj.getClass()) {
            return false;
        }
        return this.getName().equals(((ImprovedNameBean)obj).getName());
    }
    @Override
    public int hashCode() {
        return getName().hashCode();
    }
}

public class MapDuplicateTest {
    public static void main(String[] args) {
        MapDuplicateTest test = new MapDuplicateTest();
        System.out.println("Adding duplicates to a map (bad):");
        test.withDuplicates();
        System.out.println("\nAdding duplicates to a map (good):");
        test.withoutDuplicates();
    }
    public void withDuplicates() {
        NameBean bean1 = new NameBean("Alice");
        NameBean bean2 = new NameBean("Alice");

        java.util.Map<NameBean, String> map
                = new java.util.HashMap<NameBean, String>();
        map.put(bean1, "first");
        map.put(bean2, "second");
        System.out.println("Result of map.get(bean1):"+map.get(bean1));
        System.out.println("Result of map.get(bean2):"+map.get(bean2));
        System.out.println("Result of map.get(new NameBean(\"Alice\"): "
                + map.get(new NameBean("Alice")));
    }
    public void withoutDuplicates() {
        ImprovedNameBean bean1 = new ImprovedNameBean("Alice");
        ImprovedNameBean bean2 = new ImprovedNameBean("Alice");

        java.util.Map<ImprovedNameBean, String> map
                = new java.util.HashMap<ImprovedNameBean, String>();
        map.put(bean1, "first");
        map.put(bean2, "second");
        System.out.println("Result of map.get(bean1):"+map.get(bean1));
        System.out.println("Result of map.get(bean2):"+map.get(bean2));
        System.out.println("Result of map.get(new ImprovedNameBean(\"Alice\"): "
                + map.get(new ImprovedNameBean("Alice")));
    }
}

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

Если вы написали более нескольких нетривиальных библиотек на Java, вы будете знать, что равенство трудно добиться правильно, особенно когда единственными инструментами в сундуке являются equals и hashCode.Равенство в конечном итоге тесно связано с иерархией классов, что приводит к хрупкости кода.Более того, проверка типа не предусмотрена, поскольку эти методы просто принимают параметры типа Object.

Есть способ сделать проверку равенства (и хеширование) намного менее подверженной ошибкам и более типобезопасной.В Функциональная Java библиотека, вы найдете Equal<A> (и соответствующий Hash<A>), где равенство разделено на единый класс.В нем есть методы составления Equal экземпляры для ваших классов из существующих экземпляров, а также оболочки для коллекций, Повторяющиеся объекты, Хэш - карта, и Хэш - набор, которые используют Equal<A> и Hash<A> вместо того, чтобы equals и hashCode.

Что лучше всего в этом подходе, так это то, что вы никогда не можете забыть написать equals и hash-метод, когда они вызываются.Система ввода поможет вам запомнить.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top