Какие есть альтернативы сравнению равенства двух объектов? - PullRequest
3 голосов
/ 11 декабря 2008

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

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

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

EDIT:

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

Ответы [ 7 ]

4 голосов
/ 11 декабря 2008

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

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


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

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

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

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

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

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

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

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

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

  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 ). Например, что если вы хотите собрать набор файлов для разбора. По определению набор содержит только уникальные элементы. Java Set использует методы equals / hashCode для обеспечения уникальности своих элементов.

4 голосов
/ 11 декабря 2008

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

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

3 голосов
/ 11 декабря 2008

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

Вот мой шаблон 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;
 }
1 голос
/ 11 декабря 2008

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

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

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

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

1 голос
/ 11 декабря 2008

ммч

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

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

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

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

предупреждение о большом количестве кода

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

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

a == b 

верно, в то время как

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

ложно.

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

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

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

0 голосов
/ 13 декабря 2008

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

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

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

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

0 голосов
/ 12 декабря 2008

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

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

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