Как подчиняться контракту equals() при выводе из абстрактного класса

В своей книге "Эффективная Java" Джошуа Блох пишет о подводных камнях, которые происходят с контрактом equals(), когда производные классы добавляют дополнительные поля к проверке. Обычно это нарушает симметрию, но Блох утверждает, что "вы можете добавить компонент значения в подкласс абстрактного класса, не нарушая равный контракт".

Очевидно, это верно, потому что не может быть экземпляров абстрактного класса, поэтому нет никакой симметрии для нарушения. Но как насчет других подклассов? Я написал этот пример, намеренно исключая реализации hashcode и null-проверок, чтобы держать код коротким:

public abstract class Vehicle {

    private final String color;

    public Vehicle(String color) {
        this.color = color;
    }

    public String getColor() {
        return color;
    }

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

        if (!(o instanceof Vehicle)) return false;

        Vehicle that = (Vehicle) o;

        return color.equals(that.color);
    }

}

public class Bicycle extends Vehicle {

    public Bicycle(String color) {
        super(color);
    }

}

public class Car extends Vehicle {

    private final String model;

    public Car(String color, String model) {
        super(color);
        this.model = model;
    }

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

        if (!(o instanceof Car)) return false;

        Car that = (Car) o;

        return getColor().equals(that.getColor()) && model.equals(that.model);
    }

}

Когда я создаю один экземпляр каждого класса с одной и той же цветовой строкой, нарушается симметрия equals():

Bicycle bicycle = new Bicycle("blue");
Car car = new Car("blue", "Mercedes");

bicycle.equals(car) <- true
car.equals(bicycle) <- false

Я не уверен, как с этим справиться. Объявить equals() как абстрактный в абстрактном классе для обеспечения реализации в подклассах? Но такого же эффекта можно добиться, не объявив equals () вообще в абстрактном классе.

Ответ 1

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

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

Ответ 2

Сравнение объекта класса вместо выполнения проверки instanceof решает проблему.

        if (getClass() != obj.getClass()) {
            return false;
        }

Вот полная реализация (созданная Eclipse):

public class Vehicle {

    // ...    

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

}

public class Car extends Vehicle {

    // ...

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!super.equals(obj)) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Car other = (Car) obj;
        if (model == null) {
            if (other.model != null) {
                return false;
            }
        } else if (!model.equals(other.model)) {
            return false;
        }
        return true;
    }


}

Обе проверки в вашем примере затем приведут к false.

Ответ 3

Симметрия equals() в основном нарушена, потому что класс Bicycle является подклассом и зависит от суперкласса (Vehicle) для его собственного равенства. Если вы определяете метод equals() для каждого подкласса, вы не столкнетесь с этой проблемой.

Вот реализация equals() для каждого из классов. (Добавляется только Bicycle equals(), другие реализации такие же, но упрощены.)

public abstract class Vehicle {
....
      @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Vehicle)) return false;
    Vehicle that = (Vehicle) o;
    return color.equals(that.color);
  }
}

public class Bicycle extends Vehicle {
...
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Bicycle)) return false;
    Bicycle that = (Bicycle) o;
    return super.getColor().equals(that.getColor());
  }
}

public class Car extends Vehicle {
...
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Car)) return false;
    if (!super.equals(o)) return false;
    Car car = (Car) o;
    return model.equals(car.model);
  }
}

// This is main class for testing the above functionality.

class MainClass {
  public static void main(String[] args) {
    Bicycle bicycle = new Bicycle("blue");
    Car car = new Car("blue", "Mercedes");

    System.out.println(bicycle.equals(car));   -> false
    System.out.println(car.equals(bicycle));   -> false
  }
}

ИЛИ, вы должны использовать оператор object.getClass() вместо instanceof в реализации суперкласса, как это было предложено @FranzBecker. Подкласс может по-прежнему использовать оператор instanceof без каких-либо проблем.

public abstract class Vehicle {
...
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if ((this.getClass() !=  o.getClass())) return false;
    Vehicle that = (Vehicle) o;
    return color.equals(that.color);
  }
}

Ответ 4

equals() существенно сравнивает состояние объекта. При создании абстрактного класса вы должны подумать о том, как вы хотите обработать состояние объекта.

В вашем случае ваше транспортное средство имеет определенный цвет. Вопрос: Вы хотите, чтобы все транспортные средства с одинаковым цветом считались равными? Затем сделайте ответ на этот вопрос частью договора вашего абстрактного класса.

Если вы ответите ДА:

Достаточно просто, просто сделайте равным final.

Если вы ответите НЕТ:

Вы (вполне понятно) хотите, чтобы ваши равны были симметричными. Посмотрите на следующий код:

Bicycle bike = new Bicycle("blue");
Car car = new Car("blue", "gtr");
assert car.equals(bike) == bike.equals(car);

Это приведет к ошибке с AssertionError, потому что при вызове bike.equals(car) вы сравниваете по стандартам велосипеда. Чтобы решить эту проблему, вы можете использовать equals for Bicycle. Тем не менее, вы не хотите смотреть на все классы, чтобы убедиться, что кто-то не забыл реализовать где-то равные.

В этом случае достаточно убедиться, что в вашем абстрактном родителе, что разные подклассы возвращают false. Это можно сделать просто, заменив if (! (o instanceof P)) return false; на if (!getClass().equals(o.getClass())) return false;. Ваша симметрия будет сохранена.