在Java中,如果要比较两个物件的实质相等性,并不是使用==,而是必须透过equals()方法,例如:
String s1 = new String(“Java”);
String s2 = new String(“Java”);
out.println(s1 == s2);//显示false
out.println(s1.equals(s2));//显示true
两个物件是新建构出来的,所以s1与s2是参考到不同物件,因而使用==比较会是false,要比较两个字串的实质字元序列,必须使用equals(),这是因为String的equals()重新定义为比较两个字串的字元序列。
如果你定义类别时,没有重新定义equals()方法,则预设继承自Object,Object的equals()方法是定义为:
public boolean equals(Object obj){
return(this == obj);
}
也就是如果你没有重新定义equals(),使用equals()方法时,作用等同于使用==。如果你要重新定义equals(),必须注意几个地方,例如,你可能如下定义了equals()方法:
public class Point {
public final int x;
public final int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
public boolean equals(Point that){
return this.x == that.x && this.y == that.y;
}
}
如果你这么测试:
Point p1 = new Point(1,1);
Point p2 = new Point(1,1);
out.println(p1.equals(p2));//显示true
看来似乎没错,p1与p2坐标都是同一点,所以实际上指的相同的坐标,但是如果你这么测试:
Point p1 = new Point(1,1);
Point p2 = new Point(1,1);
Set pSet = new HashSet();
pSet.add(p1);
out.println(pSet.contains(p2));//显示false
Set中放入的p1与要测试的p2明明是指同一点,为什么会显示false?问题在于你没有重新定义Object的equals(),你是另外定义了一个equals()方法,参数是Point型态,换言之,你是重载(overload),不是重新定义(Override),Object的equals()接受的是Object型态的参数。如果你使用以下的程序测试,就可以知道原因:
Object p1 = new Point(1,1);
Point p2 = new Point(1,1);
out.println(p1.equals(p2));//显示false
p1是Object宣告,看不到Point中的equals(),所以就使用Object本身的equals(),结果当然是false。
在JDK5之后,可以使用@Override避免这类错误,例如:
public class Point {
public final int x;
public final int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object that){
if(that instanceof Point){
Point p =(Point)that;
return this.x == p.x && this.y == p.y;
}
return false;
}
}
再作同样的测试:
Object p1 = new Point(1,1);
Point p2 = new Point(1,1);
out.println(p1.equals(p2));//显示true
结果看来是正确了,不过:
Point p1 = new Point(1,1);
Point p2 = new Point(1,1);
Set pSet = new HashSet();
pSet.add(p1);
out.println(pSet.contains(p2));//可能显示false
如果上例结果显示false,并不用讶异,因为你在重新定义equals()时,并没有重新定义hashCode(),在许多场合,例如将物件加入群集(Collection)时,会同时利用equals()与hashCode()来判断是否加入的是(实质上)相同的物件。在Object的hashCode()说明指出:
在同一个应用程序执行期间,对同一物件呼叫hashCode()方法,必须回传相同的整数结果。
如果两个物件使用equals(Object)测试结果为相等,则这两个物件呼叫hashCode()时,必须获得相同的整数结果。
如果两个物件使用equals(Object)测试结果为不相等,则这两个物件呼叫hashCode()时,可以获得不同的整数结果。
以HashSet为例,会先使用hashCode()得出该将物件放至哪个哈希桶(hash buckets)中,如果哈希桶有物件,再进一步使用equals()确定实质相等性,从而确定Set中不会有重复的物件。上例中说可能会显示false,是因为若凑巧物件hashCode()算出在同一个哈希桶,再进一步用equals()就有可能出现true。
在重新定义equals()时,最好重新一并重新定义hashCode()。例如:
public class Point {
public final int x;
public final int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object that){
if(that instanceof Point){
Point p =(Point)that;
return this.x == p.x && this.y == p.y;
}
return false;
}
@Override
public int hashCode(){
return 41 *(41 + x)+ y;
}
}
再次测试就会得到true了:
Point p1 = new Point(1,1);
Point p2 = new Point(1,1);
Set pSet = new HashSet();
pSet.add(p1);
out.println(pSet.contains(p2));//显示true
一个重要的观念是,定义equals()与hashCode()时,最好别使用状态会改变的数据成员。你可能会想,以这个例子来说,点会移动,如果移动了就不是相同的点了,不是吗?假设x、y是个允许会变动的成员,那么就会发生这个情况:
Point p1 = new Point(1,1);
Set pSet = new HashSet();
pSet.add(p1);
out.println(pSet.contains(p1));//显示true
p1.x = 2;
out.println(pSet.contains(p1));//显示false
明明是內存中同一个物件,但置入Set后,最后跟我说不包括p1?这是因为,你改变了x,算出来的hashCode()也就改变了,使用contains()尝试比对时,会看看新算出来的哈希桶中是不是有物件,而根本不是置入p1的哈希桶中寻找,结果就是false了。
在Object的equals()说明中有提到,实作equals()时要遵守的约定:
反身性(Reflexive):x.equals(x)的结果要是true。
对称性(Symmetric):x.equals(y)与y.equals(x)的结果必须相同。
传递性(Transitive):x.equals(y)、y.equals(z)的结果都是true,则x.equals(z)的结果也必须是true。
一致性(Consistent):同一个执行期间,对x.equals(y)的多次呼叫,结果必须相同。
对任何非null的x,x.equals(null)必须传回false。
目前定义的Point,其equals()方法满足以上几个约定(你可以自行写程序测试bojincn)。现在考虑继承的情况,你要定义3D的点:
public class Point3D extends Point {
public final int z;
public Point3D(int x,int y,int z){
super(x,y);
this.z = z;
}
@Override
public boolean equals(Object that){
if(that instanceof Point3D){
Point3D p =(Point3D)that;
return super.equals(p)&& this.z == p.z;
}
return false;
}
}
这看来似乎没什么问题,3D的点要再比较z坐标是没错。不过来测试一下:
Point p1 = new Point(1,1);
Point p2 = new Point3D(1,1,1);
out.println(p1.equals(p2));//显示true
println(p2.equals(p1));//显示false
结果该是true或false需要讨论一下。3D的点与2D的点是否相等呢?假设你考虑的是点投射在xy平面上是否相等,那p1.equals(p2)为true就可以接受,在此假设之下,再来看p2.equals(p1)为false,这违反equals()对称性的对称性合约。如果你要满足对称性,则要作个修改:
public class Point3D extends Point {
public final int z;
public Point3D(int x,int y,int z){
super(x,y);
this.z = z;
}
@Override
public boolean equals(Object that){
if(that instanceof Point3D){
Point3D p =(Point3D)that;
return super.equals(p)&& this.z == p.z;
}
if(that instanceof Point){
return that.equals(this);
}
return false;
}
}
再次运行上面的测试,就可以得到都是true的结果,但如果是这个:
Point p1 = new Point(1,1);
Point p2 = new Point3D(1,1,1);
Point p3 = new Point3D(1,1,2);
out.println(p2.equals(p1));//显示true
out.println(p1.equals(p3));//显示true
out.println(p2.equals(p3));//显示false
p2等于p1,p1等于p3,但p2不等于p3,这违反传递性合约。问题点在于,2D的点并没有z轴信息,无论如何也没办法满足传递性了。
一般来说,对于不同的类别实例,会将之视为不同,基本上你可以这么设计:
public class Point {
public final int x;
public final int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object that){
if(that instanceof Point){
Point p =(Point)that;
return this.getClass()== p.getClass()&&
this.x == p.x &&
this.y == p.y;
}
return false;
}
@Override
public int hashCode(){
return 41 *(41 + x)+ y;
}
}
public class Point3D extends Point {
public final int z;
public Point3D(int x,int y,int z){
super(x,y);
this.z = z;
}
@Override
public boolean equals(Object that){
if(that instanceof Point3D){
Point3D p =(Point3D)that;
return super.equals(p)&& this.z == p.z;
}
return false;
}
}
直接判断类别,让不同类别的实例视为不相等,就这个例子而言,使得Point只能与Point比,Point3D只能与Point3D比,直接解决了不同继承阶层下equals()的合约问题。
不过在以下这种需求时,这样的定义也许不符合你的需求:
Point p1 = new Point(1,1);
Point p2 = new Point(1,1){
@Override
public String toString(){
return“(”+ x +“,”+ y +“)”;
}
};
Set pSet = new HashSet();
pSet.add(p1);
out.println(pSet.contains(p1));//显示true
out.println(pSet.contains(p2));//显示false,但你想显示true
你也许是在某处建立了个匿名类别物件,然后在程序中某处又打算测试看看Set中是否含有相同坐标的点,但结果并不是显示true,这是因为你严格地在equals()中检查了实例的类别名称。
你可以将定义改为以下:
public class Point {
public final int x;
public final int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object that){
if(that instanceof Point){
Point p =(Point)that;
return p.canEquals(this)&&
this.x == p.x &&
this.y == p.y;
}
return false;
}
public boolean canEquals(Object that){
return that instanceof Point;
}
@Override
public int hashCode(){
return 41 *(41 + x)+ y;
}
}
在equals()中,你不仅检查传入的实例是否为Point,也反过来让传入的实例取得this的型态进行测试(这是Visitor模式的实现)。而在Point3D中:
public class Point3D extends Point {
public final int z;
public Point3D(int x,int y,int z){
super(x,y);
this.z = z;
}
@Override
public boolean equals(Object that){
if(that instanceof Point3D){
Point3D p =(Point3D)that;
return p.canEquals(this)&&
super.equals(p)&& this.z == p.z;
}
return false;
}
@Override
public boolean canEquals(Object that){
return that instanceof Point3D;
}
@Override
public int hashCode(){
return 41 * super.hashCode()+ z;
}
}
如果p1是Point物件,而p2是Point3D物件,p1.equals(p2)时,由于传入的实例可以取得this的型态进行测试,p2反过来测试p1是不是Point3D,结果不是(cnoude),所以equals()传回false,利用这个方式,让有具体名称的子类别实例,不会与父类别实例有相等成立的可能性。如果是直接继承Point类别的匿名类别物件,则直接继承canEquals()方法,由于匿名类别物件还是一种Point实例,因此equals()的结果会是true。
一个测试的结果如下:
Point p1 = new Point(1,1);
Point p2 = new Point(1,1){
@Override
public String toString(){
return“(”+ x +“,”+ y +“)”;
}
};
Point p3 = new Point3D(1,1,1);
Set pSet = new HashSet();
pSet.add(p1);
out.println(pSet.contains(p1));//显示true
out.println(pSet.contains(p2));//显示true
out.println(pSet.contains(p3));//显示false
打开App,阅读手记