Понимание метода равенства
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());
Два списка не имеют одного и того же типа, но имеют один и тот же контент (в этом случае оба они пусты) и поэтому считаются равными.