Понимание метода равенства

J. Блох в своей эффективной Java предоставляет несколько правил реализации для метода equals. Вот они:

• Возвратный: Для любого ненулевого опорного значения х, x.equals(х) должны return true.

• Симметричный: для любых непустых опорных значений x и y, x.equals(y) должен возвращать true тогда и только тогда, когда y.equals(x) возвращает true.

• Transitive: для любых непустых опорных значений x, y, z, если x.equals(y) возвращает true, а y.equals(z) возвращает true, тогда x.equals(z) должно возвращать true.

• Согласовано: для любой ненулевой ссылки значения x и y, множественные вызовы x.equals(y) последовательно возвращает true или последовательно возвращает false, если не используется информация в равных сопоставлениях по объектам изменяется.

• Для любого непустого опорное значение х, x.equals(нуль) должен возвращать ложь.

Но позже в книге он упомянул так называемый принцип подписи Лискова:

Принцип подстановки Лискова гласит, что любое важное свойство тип должен также содержать для своих подтипов, так что любой написанный метод для типа должны работать одинаково хорошо по своим подтипам

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

Вопрос о реализации метода для подклассов. Вот пример из книги:

private static final Set<Point> unitCircle;

static {
    unitCircle = new HashSet<Point>();
    unitCircle.add(new Point(1, 0));
    unitCircle.add(new Point(0, 1));
    unitCircle.add(new Point(-1, 0));
    unitCircle.add(new Point(0, -1));
}

public static boolean onUnitCircle(Point p) {
    return unitCircle.contains(p);
}

public class CounterPoint extends Point {
    private static final AtomicInteger counter = new AtomicInteger();

    public CounterPoint(int x, int y) {
        super(x, y);
        counter.incrementAndGet();
    }

    public int numberCreated() { return counter.get(); }
}

и следующую реализацию:

// Broken - violates Liskov substitution principle (page 40)
@Override public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}

Хорошо, нарушает и что тогда? Я не понимаю.

Ответ 1

Обычно существует два способа проверки типа в методе equals:

Вариант 1: экземпляр

if (! (obj instanceof ThisClass)){
    return false;
}

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

Вариант 2: getClass()

if (obj == null || ! this.getClass().equals(obj.getClass())) {
    return false;
}

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

Джошуа Блох предупреждает об этом в своей книге "Эффективная Ява".

Анжелика Лангер, однако, упоминает способ сравнения "mixed-tpye", если вы можете определить значения по умолчанию для дополнительных свойств:

http://www.angelikalanger.com/Articles/JavaSolutions/SecretsOfEquals/Equals-2.html

Недостатком является то, что методы equals становятся довольно сложными.

// Broken - violates Liskov substitution principle (page 40)
@Override public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}

Хорошо, нарушает и что тогда? Я не понимаю.

Итак, если у вас есть подкласс, например MyPoint (который может добавлять дополнительные методы, но не дополнительные свойства/поля), то

Point p1 = new Point(x, y);
Point p2 = new MyPoint(x, y);

p1.equals(p2) == false

Set<Point> points = new HashSet<>();
points.add(p1);

points.contains(p2) == false;

хотя оба объекта действительно представляют одну и ту же точку.

Если вместо этого вы будете использовать опцию 1 (instanceof), метод equals вернет true.

Ответ 2

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

new Point(0, 0).equals(new CounterPoint(0, 0));

потому что две точки имеют одинаковые координаты, даже если они не имеют одного и того же типа. Но предложенный метод equals возвращает false, потому что два объекта имеют разные классы.

Если вы думаете о коллекциях, например, это верно:

new LinkedList().equals(new ArrayList());

Два списка не имеют одного и того же типа, но имеют один и тот же контент (в этом случае оба они пусты) и поэтому считаются равными.