Java equals超类和子类

3

我有以下两个非常简单的类:

   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,当且仅当y.equals(x)返回true时,x.equals(y)应返回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));
    }

}

第一个返回true,而第二个返回false。 一个看起来正确的解决方案是使用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表示车门数。当我们调用contains方法时,实质上是在询问:“我们的集合中是否包含一辆四轮车?”答案应该是肯定的,因为它确实包含了,而不管它是不是一辆汽车以及它有多少车门。 Joshua Block在Effective Java 2nd Ed pg 40中提出的解决方案是不要让B从A继承,并在B中拥有一个A的实例作为字段。
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;
    }
}

然而,这是否意味着我们在许多需要使用继承的情况下失去了使用它的能力?也就是说,当我们有一些带有额外属性的类需要扩展更通用的类以重用代码并添加它们自己的特定属性时。

你正在使用组合而不是继承。在你的情况下,如果你决定采用继承,你认为Liskov替换原则适用于B吗? - SMA
如果您根据基类中存在的一个唯一ID属性来标识您的对象,则可以避免此问题。然后,您就不必覆盖基类的equals()实现。当您比较两个车辆时,您可以比较门的数量、轮子的数量和其他1000个属性,或者您可以简单地比较车辆ID。哪个选项对您更有意义? - Eran
@Eran 使用唯一标识符比较对象确实是有意义的,但并非在所有情况下都足够。想象一下,您有一辆汽车,并在用户进入应用程序的特定部分时对其进行深度复制。然后,您让用户决定是否要改变原始汽车对象。然后,在退出此应用程序部分时,您希望检查用户是否实际上更改了对象。因此,您将其与原始副本进行比较。它们的ID显然相同,但它们的其他属性可能不同。 - Gabriel
1
这是继承具体类的一个众所周知的问题,Java日期类就是一个臭名昭著的例子。将类定义为抽象或(有效地)最终的规则有一定的优点。 - chrylis -cautiouslyoptimistic-
@SMA,如果A中的equals方法使用instanceof而不是getClass(),那么Liskov替换原则是有效的。如果我说错了,请纠正我。 - Gabriel
1个回答

1

无论收集问题如何,我认为b.equals(a)返回true非常不直观且令人困惑。

如果您希望在某个上下文中认为ab相等,则显式实现该上下文的相等逻辑。

1) 例如,如果要使用A的相等逻辑在HashSet中使用As和Bs,请实现一个包含A并实现equals方法的adapter

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(和B),从客户端代码中隐藏MyAdapter(它可以是包装类中的private static类),以提高可读性。

2)标准jdk库中的选项是使用TreeSet

请注意,集合(无论是否提供显式比较器)维护的排序必须与equals一致,如果要正确实现Set接口。(有关与equals一致的精确定义,请参见ComparableComparator。)这是因为Set接口是根据equals操作定义的,但是TreeSet实例使用其compareTo(或compare)方法执行所有元素比较,因此从集合的角度来看,通过此方法视为相等的两个元素是相等的。即使集合的排序与equals不一致,集合的行为也是明确定义的;它只是未遵守Set接口的通用契约。
因为TreeSet不依赖于equals,所以只需按照同样的方式实现适当的比较器,就像您实现MyAdapter.equals一样(如果a1.getA() == a2.getA(),则返回0)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接