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

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

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

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

РЕДАКТИРОВАТЬ:

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

11.12.2008 17:53:05
этот вопрос может использовать немного соли и некоторый контекст.
Jimmy 11.12.2008 17:58:41
Лучшее, о чем я могу думать, это то, почему я пытаюсь сравнивать объекты? Учитывая эту причину, каковы альтернативы сравнения, чтобы привести меня туда, куда мне нужно идти ... извините, я знаю, что это может не помочь.
user4903 11.12.2008 18:03:02
7 ОТВЕТОВ
РЕШЕНИЕ

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

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


Вся эта информация взята из Effective Java, Second Edition ( Джош Блох ). Первая глава издания об этом все еще доступна для бесплатной загрузки .

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

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

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

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

Фактически, редактор Java Eclipse (3.4) делает именно это, когда вы генерируете метод с использованием инструментов исходного кода. По словам Блоха, это ошибка, поскольку она нарушает принцип подстановки Лискова .

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

Нет никакого способа расширить инстанцируемый класс и добавить компонент value, сохраняя контракт equals.

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

  1. Пользуйся композицией, а не наследством
  2. Дизайн и документация для наследования или иначе запретить

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

Например, вы можете определить интерфейс, который документирует, как он должен сравниваться. В приведенном ниже коде экземпляры Service могут быть заменены во время выполнения более новой версией того же класса - в этом случае при наличии разных ClassLoaders сравнения 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);
    }
}

"Почему вы хотите сравнить два объекта?"

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

4
15.12.2016 04:49:42
Все это были отличные ответы. Причина, по которой я принял ваш ответ, заключается в том, что он ближе всего подходит к решению поставленного мною вопроса. Спасибо!
user4903 17.12.2008 13:53:34

Mmhh

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

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

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

Посмотрите на этот ответ, чтобы узнать, как реализовать такую ​​вещь.

предупреждение много кода

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

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

a == b 

правда в то время как

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

ложно

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

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

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

1
23.05.2017 12:25:24

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

Вот мой шаблон 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;
 }
3
11.12.2008 18:41:33
Будьте осторожны с этим шаблоном. Не используйте вызов super.equals, если вы не реализуете подкласс для класса, который уже реализовал equals () - в противном случае этот метод просто сводится к равенству ссылок. Также строка с приведением класса имеет синтаксическую ошибку.
Dave L. 11.12.2008 18:30:05
Вы правы, я взял это из класса, где я делал равные на подклассе
Pyrolistical 11.12.2008 18:41:58

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

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

4
11.12.2008 18:12:39

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

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

Почему вы хотите сравнить два объекта? Ну, если они равны, вы хотели бы сделать одну вещь, а если нет, вы бы хотели сделать что-то другое.

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

1
11.12.2008 19:19:20

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

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

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

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

Вывод:

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")));
    }
}
0
11.12.2008 22:13:37

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

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

Есть способ сделать проверку на равенство (и хэширование) намного менее подверженной ошибкам и более безопасной для типов. В библиотеке функциональной Java вы найдете Equal<A>(и соответствующий Hash<A>), где равенство разделено на один класс. У него есть методы для создания Equalэкземпляров для ваших классов из существующих экземпляров, а также обертки для коллекций, Iterables , HashMap и HashSet , которые используют Equal<A>и Hash<A>вместо equalsи hashCode.

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

0
13.12.2008 08:24:16