Каким должен быть результат переопределения Object#equals(Object) при повышении класса экземпляра?

Я особенно заинтересован в соблюдении части симметрии общего контракта, установленного в Object#equals(Object), где для двух ненулевых объектов x и y результат x.equals(y) и y.equals(x) должен быть одинаковым.

Предположим, у вас есть два класса, PurchaseOrder и InternationalPurchaseOrder, где последний расширяет первый. В базовом случае имеет смысл, что сравнение экземпляра каждого с другим должно последовательно возвращать false для x.equals(y) и y.equals(x) просто потому, что PurchaseOrder не всегда является InternationalPurchaseOrder и, следовательно, дополнительные поля в объектах InternationalPurchaseOrder не будут присутствовать в экземплярах InternationalPurchaseOrder. PurchaseOrder.

Теперь предположим, что вы повышали экземпляр InternationalPurchaseOrder.

PurchaseOrder order1 = new PurchaseOrder();
PurchaseOrder order2 = new InternationalPurchaseOrder();
System.out.println("Order 1 equals Order 2? " + order1.equals(order2));
System.out.println("Order 2 equals Order 1? " + order2.equals(order1));

Мы уже установили, что результат должен быть симметричным. Но должен ли результат быть false для случаев, когда оба объекта содержат одни и те же внутренние данные? Я считаю, что результат должен быть true независимо от того, что у одного объекта есть дополнительное поле. Поскольку при преобразовании order2 доступ к полям в классе InternationalPurchaseOrder ограничивается, результатом метода equals() должен быть результат вызова super.equals(obj).

Если все, что я сказал, верно, реализация метода equals в InternationalPurchaseOrder должна быть примерно такой:

@Override
public boolean equals(Object obj) {
    
     if (!super.equals(obj)) return false;
     
     // PurchaseOrder already passed check by calling super.equals() and this object is upcasted
     InternationalPurchaseOrder other = (InternationalPurchaseOrder)obj;
     
        if (this.country == null) {
            if (other.country != null) return false;
        } else if (!country.equals(other.country)) return false;

        return true;
}

Предположим, что country — единственное поле, объявленное в этом подклассе.

Проблема в суперклассе

@Override
public boolean equals (Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    PurchaseOrder other = (PurchaseOrder) obj;
    if (description == null) {
        if (other.description != null) return false;
    } else if (!description.equals(other.description)) return false;
    if (orderId == null) {
        if (other.orderId != null) return false;
    } else if (!orderId.equals(other.orderId)) return false;
    if (qty != other.qty) return false;
    return true;
}

Потому что два экземпляра не принадлежат к одному классу. И если вместо этого я использую getClass().isAssignableFrom(Class), симметрия теряется. То же самое верно и при использовании instanceof. В книге «Эффективная Java» Джошуа Блох косвенно предупреждает о ненужном переопределении equals. В этом случае, однако, необходимо, чтобы подкласс имел переопределенное equals для экземпляров для сравнения полей, объявленных в подклассе.

Это произошло достаточно давно. Является ли это аргументом в пользу более сложной реализации функции equals или это просто аргумент против повышения приведения?

ПОЯСНЕНИЕ: на этот вопрос нет ответа в предложениях, предложенных @Progman в разделе комментариев ниже. Это не простой случай переопределения методов equals для подклассов. Я думаю, что код, который я разместил здесь, показывает, что я сделал это правильно. Этот пост СПЕЦИАЛЬНО посвящен ожидаемому результату сравнения двух объектов, когда один из них преобразован в более высокий класс, чтобы он вел себя как объект суперкласса.


person hfontanez    schedule 07.03.2021    source источник
comment
Отвечает ли это на ваш вопрос? Java - метод equals в базовом классе и в подклассах   -  person Progman    schedule 07.03.2021
comment
Также проверьте stackoverflow.com/questions/596462/   -  person Progman    schedule 07.03.2021
comment
@Progman ни один из них не отвечает на этот вопрос.   -  person hfontanez    schedule 07.03.2021
comment
@user15244370 user15244370 Я думаю, что знаю это, и, кажется, я заявил об этом в посте. Вопрос конкретно в том, каким должен быть результат сравнения двух объектов, когда один из них понижен.   -  person hfontanez    schedule 07.03.2021
comment
Имейте в виду, что приведение объекта к понижению не меняет его тип. Переменная order2 по-прежнему ссылается на объект InternationalPurchaseOrder, а order2.equals(order1) вызывает метод equals() из класса InternationalPurchaseOrder. Что заставляет метод equals() возвращать false с вашего предложения [...] должен последовательно возвращать false для x.equals(y) и y.equals(x) просто потому, что PurchaseOrder не всегда является InternationalPurchaseOrder и, следовательно, [...]. Как вы планируете вернуть true, если вы также говорите, что он должен возвращать false?   -  person Progman    schedule 07.03.2021
comment
@Progman, это философский вопрос, который я поднимаю. Я думаю, что ответ заключается в том, что понижение приведения - это запах кода (для поднятой здесь проблемы) и что вместо понижения объект должен быть адаптирован (преобразован), а не понижен.   -  person hfontanez    schedule 08.03.2021


Ответы (2)


Я думаю, что неправильное представление здесь заключается в том, что преобразование на самом деле изменяет тип объекта.

Любой тип приведения — это просто указатель на то, что компилятор должен обрабатывать переменную как имеющую определенный тип, в интересах разработчиков. Таким образом, преобразование переменной с типом InternationalPurchaseOrder в PurchaseOrder может изменить то, как вы ее видите, и можете взаимодействовать с ней (как вы упомянули, поля ограничены), но переменная по-прежнему ссылается на InternationalPurchaseOrder

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

person Henry Twist    schedule 07.03.2021
comment
Верно... моя точка конфликта заключается в том, что я понимаю, почему так много людей думают о том, чтобы принизить запах кода. Это может доказать это. С другой стороны, когда вы выполняете приведение вниз, вы делаете это с целью маскировки данных в подклассе. Например, преобразование long в int. Как будто цель состоит в том, чтобы игнорировать старшие байты и просто учитывать младшие байты. Итак, если это правда, не должно ли сравнение Long и Integer давать true, если значения младших байтов совпадают? Поскольку это невозможно, единственной альтернативой, которую я вижу, является использование адаптера для преобразования, а не понижения. - person hfontanez; 08.03.2021
comment
Я все еще думаю, что это представляет собой интересный философский момент. - person hfontanez; 08.03.2021
comment
@hfontanez: Похоже, вы говорите о повышении точности, а не о понижении. (Лучше не использовать для этого int и long, учитывая, что они являются примитивами.) Преобразование чего-либо из Object в String приводит к понижению; приведение от String к Object является повышением. Строка PurchaseOrder order2 = new InternationalPurchaseOrder(); является неявным приведением вверх от InternationalPurchaseOrder к PurchaseOrder. - person Jon Skeet; 08.03.2021
comment
@JonSkeet спасибо за исправление. Я должен отредактировать пост с этим изменением. Я все еще склонен полагать, что это приносит больше вреда, чем пользы, и что это действительно запах кода. Вместо этого реальное решение состоит в том, чтобы использовать внедрение зависимостей и использовать технику, которую я проиллюстрировал в своем опубликованном ответе. Он эффективно делает то, что я считал правильным: взять базовый набор данных и сравнить два объекта. Использование метода asPurchaseOrder позволяет мне сделать именно это. Предпочитайте композицию наследованию!!! - person hfontanez; 08.03.2021

Оказывается, проблема немного глубже, чем я первоначально думал, по одной основной причине: принцип подстановки Лисков.

Мой метод PurchaseOrder#equals(Object) нарушает LSP. Поскольку InternationalPurchaseOrder расширяет PurchaseOrder, правильно будет сказать, что международный заказ на покупку IS-A. Следовательно, чтобы соответствовать LSP, я должен иметь возможность заменить один экземпляр другим без каких-либо побочных эффектов. Из-за этого строка if (getClass() != obj.getClass()) return false; полностью нарушает этот принцип.

Даже когда я сосредоточился на восходящем приведении (к настоящему моменту я почти уверен, что это запах кода), возникает почти та же проблема: должен ли я сравнивать экземпляры PurchaseOrder и InternationalPurchaseOrder и возвращать true, если они содержат одни и те же данные. внутри? Согласно книге Джошуа Блоха Effective Java, пункт 10 (соблюдайте общий контракт при переопределении равенства), ответ должен быть утвердительным, но на самом деле это не так; но не по тем причинам, о которых я думал изначально (они не одного типа). Проблема вот в чем, цитирую:

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

Он продолжает получать более подробную информацию об этом. Вывод прост: делайте так, и вы потеряете преимущества абстракции и нарушите LSP в процессе. Или... просто следуйте очень важному принципу программирования (особенно в Java): предпочтите композицию наследованию и вставьте атрибуты, необходимые для создания PurchaseOrder внутреннего или международного в этом примере. В моем случае это решение будет выглядеть так (несущественные детали класса опущены):

public class InternationalPurchaseOrder {
    private PurchaseOrder po;
    private String country;

    public (PurchaseOrder po, String country) {
        this.po = po;
        this.country = country;
    }

    public PurchaseOrder asPurchaseOrder() {
        return po;
    }

    @Override
    public boolean equals (Object obj) {
        if (!(obj instanceof InternationalPurchaseOrder)) {
            return false;
        }

        InternationalPurchaseOrder ipo = (InternationalPurchaseOrder)obj;
        return ipo.po.equals(po) && cp.country.equals(country);
    }
}

Таким образом, InternationalPurchaseOrderможет продолжать сравниваться с другими экземплярами того же класса, И при необходимости сравнения с объектами типа PurchaseOrder все, что нужно, — это вызвать asPurchaseOrder для возврата объекта типа совместимый тип.

person hfontanez    schedule 08.03.2021