本篇文章参考自《Effective Java》第三版第十条"Obey the general contract when overriding equals"
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
前文已经讲到了"值类"具备"逻辑相等"的特点,但是注意并不是所有的"值类"都需要重载equals()方法,例如在"用静态工厂方法代替构造器"中我们提到了Boolean类的静态构造方法valueOf(booean b),它返回两个固定的类实例new Boolean(true)和new Boolean(false),若能够确保每个值至多只存在一个实例,则不需要重载equals()方法
枚举类型是其中一个特例
One kind of value class that does not require the equals method to be overridden is a class that uses instance control to ensure that at most one object exists with each value
上述分别是自反性、对称性、传递性、一致性和x.equals(null) == false
当我们重载equals()方法时,应该问自己三个问题:它是否是对称的、传递的、一致的
我们无法在扩展(继承)可实例化的类的同时,既增加新的值组件,同时又保留equals约定,这在Java的Timestamp类中也有体现,Timestamp类继承了java.util.Date类,下面是源码注释
The Timestamp.equals(Object) method never returns true when passed an object that isn‘t an instance of java.sql.Timestamp, because the nanos component of a date is unknown. As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashCode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.
原文还指出了一种错误的写法例子,首先是父类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;
}
@Override public int hashCode() {
return 31 * x + y;
}
}
其次是子类ColorPoint类的声明
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
// Broken - violates symmetry
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
当ColorPoint类的实例调用equals()方法和Point类的实例进行比较时,总是返回false,显然违背了对称性
但是我们可以通过"单向关联",将原本的"父类"作为成员变量加入到原本的"子类"中,通过非继承的方式,来遵守equals()方法的约定
// Adds a value component without violating the equals contract
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
/**
* Returns the point-view of this color point.
*/
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);
}
@Override public int hashCode() {
return 31 * point.hashCode() + color.hashCode();
}
}
倘若equals()方法依赖于不可靠的资源,可能会违背"一致性"约定,因为在不同的场合下调用equals()方法时,可能会有不同的结果
例如Java中的URL类,在源码注释中我们可以看到equals方法被标注为不符合"一致性"约定
The defined behavior for equals is known to be inconsistent with virtual hosting in HTTP
因为URL类实例的比较涉及IP地址是否相等,而IP地址可能会在不同网络环境下发生变化,所以是"不可靠"资源,无法满足"一致性"约定
上述提到的AbstractSet类的equals()方法就是一个很好的例子
public boolean equals(Object o) {
// Use the == operator to check if the argument is a reference to this object
if (o == this)
return true;
// Use the instanceof operator to check if the argument has the correct type
if (!(o instanceof Set))
return false;
// Cast the argument to the correct type
Collection<?> c = (Collection<?>) o;
// check if that field of the argument matches the corresponding field of this object
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
特别的,对于浮点型数值的比较,Java提供了Float.compare()和Double.compare()方法来考虑Float.NaN、-0.0f和0.0f这样特殊的情况,而它们重载的equals()方法没有对这种情况进行考虑,下面是equals()方法的源码注释
If f1 and f2 both represent Float.NaN, then the equals method returns true, even though Float.NaN==Float.NaN has the value false.
If f1 represents +0.0f while f2 represents -0.0f, or vice versa, the equal test has the value false, even though 0.0f==-0.0f has the value true.
因此,在比较两个值类实例的大小前,还要注意它的特殊取值
public class TestAutoEquals {
private String username;
private int age;
private boolean male;
private String password;
}
idea提供了不同的"自动化"实现方式
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestAutoEquals that = (TestAutoEquals) o;
if (age != that.age) return false;
if (male != that.male) return false;
if (!username.equals(that.username)) return false;
return password.equals(that.password);
}
用getClass代替instanceof运算符来判断是否是同一个类的实例,尽管这样做能够解决上述"There is no way to extend an instantiable class and add a value component while preserving the equals contract"的问题,但有时候不能够采用这种替换方案,例如继承自同一个接口的不同实现类之间的比较或是父类和子类之间的比较
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestAutoEquals that = (TestAutoEquals) o;
return new EqualsBuilder()
.append(age, that.age)
.append(male, that.male)
.append(username, that.username)
.append(password, that.password)
.isEquals();
}
由特殊的Builder模式实现对每个成员变量的相等判断
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestAutoEquals that = (TestAutoEquals) o;
return age == that.age &&
male == that.male &&
Objects.equal(username, that.username) &&
Objects.equal(password, that.password);
}
此处的Objects可以来自java.util包,也可以来自com.google.common.base包
另外可以由Google开发的AutoValue框架自动生成equals()方法,并且可以结合builder构建者模式,将在下一篇文章中讲解
Effective Java —— 覆盖equals时遵守通用约定
原文:https://www.cnblogs.com/kuluo/p/12860110.html