如果不需要覆盖equals方法,那么就无需担心覆盖equals方法导致的错误。
什么时候不需要覆盖equals方法?
1.类的每个实例本质上是唯一的。
例如对于Thread,Object提供的equals实现正好符合。
2.不关心类是否提供了“逻辑相等”的测试功能。
例如Random类提*生随机数的能力,如果覆盖equals,目的该是检查两个Random实例是否产生了相同的随机数列,但实际上这个比较功能是不需要的,所以从Object继承的equals是足够的。
3.超类已经覆盖了euqlas,从超类继承过来的行为对于子类也是合适的。
例如,Set实现都从AbstractSet继承euqlas实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。
4.类是私有的或者包级私有,可以确定它的equals方法永远不会被调用。
这种时候好的做法应该覆盖equals方法,以防被意外调用:
@Override
public boolean equals(Object o) {
throw new AssertionError();
}
什么时候应该覆盖equals方法?
类具有自己特有的“逻辑相等”概念(不同于对象等同),而且超类没有覆盖equals以实现期望行为,这时需要覆盖equals方法。
通常这种类是“值类”,仅仅表示值的类,如Integer,Date,在利用equals方法时比较对象引用时,希望知道它们在逻辑上是否相等(值是否相等),而不是它们是否指向同一个对象。
一种特殊的“值类”,实例受控确保“每个值至多只存在一个对象”的类,如枚举类型,对于这样的类,逻辑等同域对象等同是同样的,因此Object的equals方法就能满足,无需覆盖。
equals有一系列的通用约定,在覆盖equals方法时,必须遵守这些约定,否则在使用jdk提供的映射表,集合等类时会导致奇怪的错误。
1.自反性,对于任何非null的引用值x,x.equals(x)必须返回true。
2.对称性,对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
3.传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true。
4.一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用信息没有被修改,那么多次调用x.equals(y)就会一致地返回true或一致地返回false。
对于任何非null的引用值x,x.equals(null)必须返回false。
解释约定:
1.自反性,要求对象必须等于自身,假如一个类违背这一点,把该类的实例添加到集合中,该集合的contain方法会告诉你该集合不包含刚刚添加的实例,这种情况一般不会出现。
2.对称性,对于任何两个对象是否相等,必须保持一致,考虑下面一个不区分大小写的字符串的类:
public final class CaseInsensitiveString {
private String s; public CaseInsensitiveString(String s) {
if(s == null) {
throw new NullPointerException();
}
this.s = s;
} @Override
public boolean equals(Object o) {
if(o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if(o instanceof String) {
return s.equalsIgnoreCase((String) o);
}
return false;
} }
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
System.out.println(cis.equals(s));
System.out.println(s.equals(cis));
cis.equals(s)返回true
s.equals(cis)返回false
问题在于CaseInsensitiveString类中的equals方法知道String对象,而String类中的equals方法却不知道CaseInsensitiveString,因此违反了对称性。
看看String的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;
}
String的equals不知道CaseInsensitiveString是一个不区分大小写的类,只是把它当成一个Object或String。
解决这个问题的方法是把与String互操作的这段代码从equals方法中去掉。
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIngnoreCase(s);
}
这样的CaseInsensitiveString的equals方法返回true必须它比较的对象是CaseInsensitiveString,如果比较对象不是CaseInsensitiveString,比如是String,那么它一定会返回false。
3.传递性,如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。
考虑子类增加的信息会影响到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;
} @Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
} }
如果直接从Point继承equals,颜色信息就会被忽略掉,所以覆盖equals实现颜色信息比较。
问题在于比较普通点和有色点时,调用普通点的equals去比较有色点,如果x,y相等,那么返回true,调用有色点的equals去比较普通点,总是返回false,不符合的对称性。
修正对称性:
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))//如果比较对象不是Point或其子类,总是返回false
return false; if(!(o instanceof ColorPoint))//如果比较对象是普通点,使用普通点的比较方法
return o.equals(this); return super.equals(o) && ((ColorPoint)o).color == color;//如果是有色点,用Point的比较方法比较x和y同时比较颜色信息
}
这种方法提供了对称性,但是牺牲了传递性:
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)和p2.equals(p3)都返回true,但p1.equals(p3)则返回false,违反传递性。前面两种比较不考虑颜色,而第三种比较则考虑了颜色。
在equals方法中用getClass测试代替instanceof测试,可以扩展可实例化的类和增加新的组件,同时保留equals约定:
@Override
public boolean equals(Object o) {
if(o == null || o.getClass() != getClass())
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
只有当对象具有相同的实现时,才能使对象等同,这样的话,p1.equals(p2),p2.equals(p3)和p1.equals(p3)都返回false,符合传递性
下面编写一个方法,检验某个整值点是否在单位圆中:
private static final Set<Point> unitCircle;
static {
unitCircle = new HashSet<Point>();
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);
}
但是假设通过某种不添加值组件的方式扩展Point,例如让构造器记录创建了多少个实例:
public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger(); public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet();
} public int numberofCreated() {
return counter.get();
}
}
根据里氏替换原则,一个类型的重要属性也将适用于它的子类型,但是,如果将CounterPoint实例传给onUnitCircle方法,如果Point类使用了基于getClass的equals方法,无论CounterPoint的x和y值是什么,onUnitCircle都会返回false,但是如果在Point上使用基于instanceof的equals方法,当遇到CounterPoint时,相同的OnUnitCircle方法就会工作得很好。
所以没有一种方法可以满足既扩展不可实例化的类,又增加值组件。根据复合优先于继承原则,不再让ColorPoint扩展Point,而是在ColorPoint中加入一个私有的Point域,以及一个公共视图方法,此方法返回一个与该有色点处于相同位置的普通Point对象:
public class ColorPoint {
private final Point point;
private final Color color; public ColorPoint(int x, int y, Color color) {
if(color == null) {
throw new NullPointException();
point = new Point(x, y);
this.color = color;
} public Point asPoint() {
return point;
} @Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
注意,可以在一个抽象类的子类中增加新的值组件,而不会违反equals约定,只要不可能直接创建超类的实例,前面的种种问题都不会发生。
4.一致性,如果两个对象相等,那么它们必须始终保持相等,除非它们有一个对象被修改了。可变对象在不同时候可以与不同的对象相等,而不可变对象则不能,相等的对象永远相等,不想等的对象永远不相等。
无论类是否可变,都不要使equals依赖于不可靠的资源。如java.net.URL的equals方法依赖于URL中主机IP地址的比较,而将一个主机名转成IP可能需要访问网络,而网络的资源是不确定的,所以无法保证产生相同结果。
显示地通过一个null测试来实现对于任何非null的引用值x,x.equals(null)必须返回false是不必要的:
@Override public boolean equals(Object o) {
if(o == null)
return false;
}
因为为了测试等同性,equals方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的域,在进行转换之前,equals必须使用instanceof操作符来检查其参数是否为正确的类型。如果比较对象是null,在instanceof的类型检查测试就不可能通过。
实现高质量equals方法的诀窍:
1.使用==操作符检查”参数是否为这个对象的引用“,如果比较操作代价很大,就值的这么做。
2.使用instanceof操作符检查”参数是否为正确的类型“,一般来说正确的类型指equals方法所在的类,某些情况下,是指该类所实现的某个接口,如果类实现的接口改进了equals约定,允许在实现了该接口的类之间进行比较,就使用接口。集合接口如Set,List,Map具有这样的特性。
3.把参数转换成正确的类型。
4.对于类中每个”关键域“,检查参数中的域是否与该对象中对应的域相匹配,域的比较顺序可能会影响性能,应该最先比较最可能不一致的域,或者开销低的域,不属于对象逻辑状态的域一般不比较,如果”冗余域“代表了整个对象的综合描述,同时比较冗余域的开销比比较所有关键域的开销小,那么比较冗余域可以节省比较失败时去比较实际数据所需要的开销。
5.当覆盖了equals方法后,测试是否符合equals的通用约定。
6.覆盖equals时总要覆盖hashCode。
7.不要企图让equals方法过于智能,过度地寻求各种等价关系,容易造成麻烦,如File类不应该把指向同一文件的符号链接当作相同的对象来看待。
8.不要将equals声明中的Object对象替换为其他类型,这会造成没有覆盖,而是重载,只要两个方法返回同样结果,那么这样是可以接受的,但与增加的复杂性相比,不值得。
@Override注解可以防止本想覆盖而错写成重载的方法,如果你的目的是覆盖,就使用该注解,这样在你出错的时候,能提示你你写的方法并不是一个覆盖的方法。