第8条:覆盖equals时请遵守通用约定

时间:2022-12-12 16:03:22

第8条:覆盖equals时请遵守通用约定


无需覆盖

    覆盖equlas方法盘起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等。如果满足了下面任何一个条件,就是所期望的结果:

 1. 类的每个实例本质上都唯一。对于代表活动实体而不是值的类来说的确如此,例如Thread。

 2. 不关心类是否提供了“逻辑相等(logical equality)”的测试功能。例如,java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机数序列,但是设计者并不认为客户需要或者期望这样的功能。在这样的情况下,从Object继承得到的equals实现已经足够了。

 3. 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。例如,大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。

 4. 类是私有的或是包级私有的,可以确定它的equals方法永远也不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防止被意外调用:

@Override
public boolean equals(Object o) {
throw new AssertionError();
}

需要覆盖

    如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这是我们就需要覆盖equals方法。这通常属于”值类(value class)”的情形。值类仅仅是一个表示值的类,例如Integer或者Date等。我们在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向了同一个对象。在这种情况下需要覆盖equals方法。

    有一种”值类”不需要覆盖equals方法,即实例受控确保”每个值之多只存在着一个对象”,也就是我们在之前说的单例(singleton)。枚举类型就属于这种类,对于这种类而言,逻辑相同与对象等同是一回事,因此Object的equals方法等同于逻辑意义上的equals方法。下面是Obejct类里面实现的equals方法:

public booleasn equals(Object obj) {
return (this == obj);
}

    在覆盖equals方法的时候,你必须要遵守它的通用约定。下面是约定的内容,来自Object的规范[JavaSE6]:

  equals方法实现了等价关系(equivalence relation) :

  1. 自反性(reflexive)。对于任何非null的引用值x,x.equals(x)必须返回true。

  2. 对称性(symmetric)。对于任何非null的引用值x和y,当且仅当y.equals(x)返回时,x.equals(y)必须返回true。

  3. 传递性(transitive)。对于任何非null的引用值x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)也是true。

  4. 一致性(consistent)。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有修改,多次调用x.equals(y)就会一致返回相同的结果。

  5. 对于任何非Null的引用值x,x.equals(null)必须返回false

    由于在一个程序中,没有哪个类是孤立的。一个类的实例通常会被频繁的传递给另一个类的实例。因此所有的类都遵循了equals的约定,这样类与类之间才能更好的合作,避免不必要的麻烦。

    对上面的五条要求,分开分析并举例:

自反性(reflexivity)对象必须等于其自身。很难想象会无意识的违反这一条,如果违背了这条规定,然后把该类的实例添加到集合(collection)中,该集合的contains方法将果断的告诉你= =,该集合并不包含你刚才添加的实例。

对称性(symmetry)任何两个对象“他们是否相等”的问题都必须保持一致。看下面的这个例子:

public class Main {

private String s;

public Main(String str) {
if (null == str) throw new NullPointerException();
s = str;
}

@Override
public boolean equals(Object obj) {
if (obj instanceof Main)
return s.equalsIgnoreCase(((Main) obj).s);
if (obj instanceof String)
return s.equalsIgnoreCase((String)obj);
return false;
}

public static void main(String[] args) {

Main a = new Main("Polish");
String s = "Polish";

System.out.println("a - s : " + a.equals(s));
System.out.println("s - a : " + s.equals(a));
}
}
输出:
a - s : true
s - a : false

    上面的例子中,输出的结果不同,原因是Main.java里面的equals()方法的实现和String.java里面的equals()方法不同,下面是String.java类实现的equals()方法:

public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

    为了解决这个问题,只需要将上面的代码改为如下即可:

@Override
public boolean equals(Object o) {
return o instanceof Main &&
((Main) o).s.equalsIgnoreCase(s);
}

传递性(transitivity),如果一个对象等于第二个对象,并且第二个对象等于第三个对象。考虑下面这种情况,考虑一个子类将一个新的值组件(value component)添加到了超类中。也就是,子类增加地信息会影响到equals的比较结果。我们首先以一个简单的不可变地二维整数型Point类作为开始:

public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
}

    现在对这个类进行扩展:

public class ColorPoint extends Point {
private final Color color
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}

    如果在上面的子类完全不提供equals方法而是直接从Point继承过来,在equals作比较的时候就会忽略color这个颜色信息。虽然没有违反equals的涉及规范,但是会出现两个颜色不同的对象会被认为是equals的。假设我们编写了一个equals方法如下:

@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}

    按照上面这种写法的equals,会在比较父类与子类实例的时候出现问题,原因是违反了第二条规则对称性,在父类中对子类的情况没有做处理,而子类对父类进行了处理,两种处理出的结果肯定不同。对于这种处理方法在上面也将过了,参照第二种规范的规范处理。、

    然而需要注意的是,上面的这种方法虽然解决了对称性的问题,但是却牺牲了传递性,在如下的测试用例情况下,就会出现关于传递性的错误:

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

    此时,p1.equals(p2)返回true,p2.equals(p3)也返回true,但是p1.equals(p3)的时候却返回了false。事实上这是面相对象语言中关于等价关系的一个基本问题。我们无法在扩展可实例化的类的同时,既增加新的值组件,又保留equals的规范约定,除非我们放弃面向对象的抽象所带来的优势。

    下面的这种方法可能会让上面的冲突消失,传递性得到了保证,但是在某些情况下会出现另外一种冲突:

@Override
public boolean equals(Object o) {
if (null == o || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}

    如果是Point类的equals按照上面的方法实现,则在下面这个例子下,就又会出现新的问题咯。

class Point {

private int x;
private int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

@Override
public boolean equals(Object obj) {
if (null == obj || obj.getClass() != getClass())
return false;
Point p = (Point) obj;
return p.x == x && p.y == y;
}
}

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();
}
}

class UnitCircle {

private static final List<Point> unitCircle;

static {
unitCircle = new ArrayList<>();
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 Main {

public static void main(String[] args) {

Point p1 = new Point(1, 0);
Point p2 = new Point(0, 1);
CounterPoint cp3 = new CounterPoint(-1, 0);
CounterPoint cp4 = new CounterPoint(0, -1);

System.out.println("p1: " + UnitCircle.onUnitCircle(p1));
System.out.println("p2: " + UnitCircle.onUnitCircle(p2));
System.out.println("cp3: " + UnitCircle.onUnitCircle(cp3));
System.out.println("cp4: " + UnitCircle.onUnitCircle(cp4));

}
}

输出结果:
p1: true
p2: true
cp3: false
cp4: false

    无论怎么检验x,y坐标存在于List中的点,总是会返回false,这是因为List在调用Point中的equals方法时,已经把除了Point类之外的类全部都屏蔽掉了,也就是我们上面的那种做法,虽然符合前三条规则,但是还是存在缺陷的。

能满足前三条规则的权宜之计

    虽然没有一种完美的办法,但下面这种方法是不错的权宜之计。根据组合优于继承,不再扩展Point类,而是将Point变成一个私有域,再给这个私有域加上一个对外公开的接口。

public class ColorPoint {
private final Point point;
private final Color color;

public ColorPoint(int x, int y, Color color) {
if (null == color)
throw new NullPointerException();
point = new Point(x, y);
this.color = color;
}

public Point asPoint() {
return point;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}

一致性(consistency),如果两个对象相等,它们就必须始终保持相等,除非他们中有一个对象(或者两个)被改变了。换句话说,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。无论类是否是不可变的,都不要使equals()方法依赖于不可靠的资源。如果违反了,想要满足一致性就非常的困难了。例如,java.net.URL的equals方法依赖于对URL中主机IP地址的比较。相关代码如下:

public boolean equals(Object obj) {
if (!(obj instanceof URL))
return false;
URL u2 = (URL)obj;
return handler.equals(this, u2);
}

//继续代码跟踪handler.equals(this, u2);

protected boolean equals(URL u1, URL u2) {
String ref1 = u1.getRef();
String ref2 = u2.getRef();
return (ref1 == ref2 ||
(ref1 != null && ref1.equals(ref2))) &&
sameFile(u1, u2);
}

非空性(Non-nullity),所有的对象都必须不等于null。

高质量equals方法总结

  1. 使用==操作符检查“参数是否为这个对象的引用”ref1 == ref2
  2. 使用instanceof操作符检查“参数是否为正确的类型”o instanceof ColorPoint
  3. 把参数转换成正确的类型ColorPoint cp = (ColorPoint) o;
  4. 对于该类中的每个“关键域”,徐检查参数中的域是否与对象的域相匹配return cp.point.equals(point) && cp.color.equals(color)
  5. 当编写完equals方法后,应该检查是否满足对称性,传递性以及一致性