我有以下两个非常简单的类:
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;
}
}
这最初似乎是正确的,但在下面的情况下,它违反了Object规范的一般契约的对称原则,该原则规定: "它是对称的:对于任何非空引用值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是一个Vehicle类而B是一个Car类,其中a是轮子的数量,而字段b是门的数量。当我们调用contains方法时,我们基本上会问:&#34;我们的套装是否包含一个四轮车?&#34;答案应该是肯定的,因为它确实包含它,无论它是否是汽车及其拥有的门数。
Joshua Block在Effective Java 2nd Ed第40页中建议的解决方案是不要让B继承自A并且将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 :(得分: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
s),从客户端代码隐藏MyAdapter
(它可以是{{1}包装类中的类)以提高可读性。
2)标准jdk库中的一个选项是使用TreeSet
:
注意由一组维护的排序(无论是否显式 提供比较器)如果是,则必须与
private static
一致 正确实现equals
接口。 (见Set
或Comparable
用于与Comparator
一致的精确定义。) 这是因为equals
接口是根据而定义的Set
操作,但equals
实例执行所有元素 使用其TreeSet
(或compareTo
)方法进行比较,两个 从这个角度来看,这种方法被视为相等的元素 集合,等于。集合的行为即使它的定义也很明确 排序与equals不一致;它只是没有服从 Set接口的一般合约。
由于compare
不依赖于TreeSet
,只需按照您实施equals
的相同方式实施正确的比较器(如果MyAdapter.equals
,则返回0
) ;