2015-08-09 2 views
3

У меня есть следующие два очень простых классов:Java равно супер и подклассов

public class A { 

    private int a; 

    public A(int a) 
    { 
    this.a=a; 
    } 
    public int getA(){ 
     return a; 
    } 
    public boolean equals(Object o) 
    { 
     if (!(o instanceof A)) 
      return false; 
     A other = (A) o; 
     return a == other.a; 
    } 
} 

И его подкласс:

public class B extends A{ 
    private int b; 
    public B(int a, int b) 
    { 
     super(a); 
     this.b = b; 
    } 
    public boolean equals(Object o) 
    { 
     if (!(o instanceof B)) 
      return false; 
     B other = (B) o; 
     return super.getA() == other.getA() && b == other.b; 
    } 
} 

Это может показаться право изначально, но в следующем случае это нарушает принцип симметрии общего контракта спецификации для объекта, который гласит: «Он симметричен: для любых непустых опорных значений x и y x.equals (y) должен возвращать истинное тогда и только тогда, когда y.equals (x) возвращает true. " http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#equals(java.lang.Object) Неисправный случаем является следующее:

public class EqualsTester { 

    public static void main(String[] args) { 
     A a = new A(1); 
     B b = new B(1,2); 
     System.out.println(a.equals(b)); 
     System.out.println(b.equals(a)); 
    } 

} 

Первые возвращает истину, в то время как второй возвращает ложь. Одним из решений, которое может показаться правильным, было бы использовать getClass() вместо instanceof. Однако это было бы неприемлемо в тех случаях, когда мы ищем B в коллекциях. Например:

Set<A> set = new HashSet<A>(); 
set.add(new A(1)); 

Метод set.contains(new B(1,2)); вернет false. Этот пример как есть, может быть, не идеален для логической визуализации, но представьте, что A был классом транспортного средства, а B - классом автомобилей, а поле a - числом колес, а поле b - числом дверей. Когда мы называем метод сложения, мы в основном спрашиваем: «В нашем наборе содержится 4-колесная машина?» Ответ должен быть «да», поскольку он содержит его, независимо от того, является ли это автомобилем и количеством дверей, которые он имеет. Решение Джошуа Блох предлагает в Effective Java 2 Ed стр 40 не иметь B наследует от A и иметь экземпляр в качестве поля в B вместо:

public class B { 
    private A a; 
    private int b; 
    public B(int a, int b) 
    { 
     this.a = new A(a); 
     this.b = b; 
    } 

    public A getAsA() 
    { 
     return A; 
    } 
    public boolean equals(Object o) 
    { 
     if (!(o instanceof B)) 
      return false; 
     B other = (B) o; 
     return a.getA() == other.getA() && b == other.b; 
    } 
} 

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

+0

Вы используете композицию вместо наследования. В вашем случае, как вы думаете, принцип замещения Лискова применяется для B, если вы решите пойти на наследование? – SMA

+0

Вы можете избежать этой проблемы, если идентифицируете свои объекты на основе одного уникального свойства ID, которое существует в базовом классе. Тогда вам не нужно переопределять реализацию equals() базового класса. Когда вы сравниваете два автомобиля, вы можете сравнить количество дверей и количество колес и 1000 других свойств, или вы можете просто сравнить идентификаторы vehice. Какой вариант имеет больше смысла для вас? – Eran

+0

@Eran Имеет смысл использовать уникальные идентификаторы для сравнения объектов, но этого недостаточно в каждом случае. Представьте, что у вас есть автомобиль, и вы делаете глубокую копию, как только пользователь вводит определенную часть вашего приложения. Затем вы разрешаете пользователю решить, хочет ли он/она изменить этот оригинальный объект автомобиля. Затем, выходя из этой части приложения, вы хотите проверить, действительно ли пользователь изменил объект или нет. Таким образом, вы сравниваете его с оригинальной копией. Очевидно, что их идентификаторы будут одинаковыми, но их другие свойства могут отличаться. – eagerToLearn

ответ

1

Независимо от проблем с коллекцией, я бы счел это крайне неинтуитивным и запутанным для b.equals(a), чтобы вернуть true.

Если вы хотите, чтобы a и b были равны в некотором контексте, тогда явно реализуйте логику равенства для этого контекста.

1) Например, чтобы использовать A с и B с в HashSet, опираясь на равенство логики A, реализовать adapter, который будет содержать A и реализовать equals метод:

class MyAdapter { 
    private final A a; 

    MyAdapter(A a) { 
     this.a = a; 
    } 

    public boolean equals(Object o) { 
     if (!(o instanceof MyAdapter)) { 
      return false; 
     } 
     MyAdapter other = (MyAdapter) o; 
     return a.getA() == other.a.getA(); 
    } 
} 

Затем просто добавить адаптер объектов в наборе:

Set<MyAdapter> set = new HashSet<>(); 
set.add(new MyAdapter(new A(1))); 

Затем set.contains(new MyAdapter(new B(1,2))); возвращает true.

Конечно, вы можете написать класс-обертку и передать его A s (и B) непосредственно, скрывая MyAdapter от клиентского кода (это может быть private static класса в класс-оболочку) для лучшей читаемости.

2) Опцион из стандартных библиотек JDK является использование TreeSet:

Обратите внимания, что порядок поддерживается с помощью набора (предоставляются ли или нет явного компаратора) должен быть совместим с equals если он правильно реализовать интерфейс Set. (См Comparable или Comparator для точного определения в соответствии с equals.) Это так, потому что интерфейс Set определен в терминах equals операции, но TreeSet экземпляр выполняет все элементы, сравнения, используя его compareTo (или compare), поэтому два элемента , считающиеся равными этому методу, с точки зрения набора равны. Поведение набора хорошо определено, даже если его порядок не согласуется с равными; он просто не подчиняется генеральному контракту интерфейса Set.

Поскольку TreeSet не полагается на equals, просто осуществить надлежащий компаратор таким же образом вы реализованный MyAdapter.equals (вернуть 0 если a1.getA() == a2.getA());

Смежные вопросы