文章目录
equals()
什么时候不需要重写
- 一个类的每个实例本质上就是不可以相同的。例如Thread
- 不需要关心一个类是否"逻辑相等"
- 超类已经改写并且超类的行为对于子类来说也是合适的
- 一个类的equals方法在程序运行时永远都不会被调用
什么时候需要重写
一个类需要有自己特有的"逻辑相等"概念,而且超类也没有改写equals来实现期望的行为或实现的行为不适用(例如Date与其子类Timestamp)
重写equals时的规范约定
- 自反性:x.equals(x)为true
- 对称性:x.equals(y)为true <=> y.equals(x)为true
public class CaseInsensitiveString{
private String s;
public CaseInsensitiveString(String s)throws Exception{
if(s == null){
throw new NullPointerException();
}
this.s = s;
}
@override
public boolean equals(Object o){
if(o instanceof CaseInsensitiveString){
return s.equalsIngnoreCase(((CaseInsensitiveString)o).s);
}
if(o instanceof String){
return .equalsIngnoreCase((String)o);
}
return false;
}
}
- 传递性: x.equals(y)为true, 且y.equals(z)为true <=> x.equals(z)为true
以一个二维点类为例, 若采用以下写法,则有色点作比较时会忽略颜色信息
// 普通点类
public class Point{
private int x;
private int y;
public void Point(Object o){
if(!(o instanceof Point)){
return false
}
Pint p = (Point)o;
return p.x == x && p.y == y;
}
}
// 有色点类
public class ColorPoint extends Point{
private Color color;
public void ColorPoint(int x, int y, Color color){
super(x, y);
this.color = color;
}
}
```
因此需要在子类重写equals()方法,若采用以下重写方法, 则违反了 __对称性__
```Java
public boolean equals(Object o){
if(!(o instanceof ColorPoint)){
return false;
}
ColorPoint cp = (ColorPoint)o;
return super.equals(o) && cp.color == this.color;
}
Point p = new Point(1, 1);
ColorPoint q = new ColorPoint(1, 1, red);
p.equals(q); // true
q.equals(p; // false
因此在子类采用一下改写方法, 但违背了 传递性
public boolean equals(Object o){
if(!(o instance of Point)){
return false
}
if(!(o instanceof ColorPoint)){
return o.equals(this);
}
ColorPoint cp = (ColorPoint)o;
return super.equals(o) && cp.color == this.color;
}
ColorPoint x = new ColorPoint(1, 1, red);
Point y = new Point(1, 1);
ColorPoint z = new ColorPoint(1, 1, blue);
x.equals(y); // true
Y.equals(Z); // true
X.equals(z); // false
根据以上,要想在扩展一个可实例化类的同时,既要增加新的特征,同时还要保留equals约定,没有一个简单的办法可以做到这一点。
因此根据建议: 复合优先于继承,类解决问题
public class ColorPoint{
private Point point;
private Color color;
public ColorPoint(int x, int y, Color color){
point = new Point(x, y);
this.color = color;
}
public boolean equals(Object o){
if(!(o instanceof ColorPoint)){
return false;
}
ColorPoint cp = (ColorPoint)o;
return cp.point.equals(point) && cp.color.euals(color);
}
}
-
一致性: 在程序中若x,y未发生变化,那么多次调用x.equals(y)返回结果是相同的
-
非空性: x.equals(y)一定是false
// 在equals()中进行两个对象逻辑判断前,前确认比较的对象是否为空
public boolean equals(Object){
if(0 == null){
return false;
}
.....
}
如何正确改写equals()
根据以上特性,总结:
- 判断实参是否为空
- 使用==操作符检查“实参是否为指向对象的引用”
- 使用instanceof操作符检查“实参是否为正确的类型”
- 把实参转换到正确的类型
- 把该类中每一个 "关键"域,检查实参中的域与当前对象中对应的阈值是否匹配。
- 写完后,检查对称性、传递性、一致性
- 要重写hashCode()方法
hashCode()
什么时候重写
在每个改写了equals方法的类中也必须改写hashcode方法
原因:如果不这样做的话,会违反ObjectCode的通用约定,从而导致该类无法与所有基于散列值(hash)的集合结合在一起正常运作。
java.lang.Object规范中,hashcode的约定:
- 在一次运行期间,同一个对象拥有通过一个散列码,对象equals()中用到的信息一旦被修改,则对象也改变。在多次运行时,一个对象的散列码可以是不同的
- 相等的对象必须具有相等的散列码。如果两个对象根据equals()方法是相等的,那么两个对象中任意对象的hashCode()犯法必须参数同样的整数结果
- 不等的对象可以有相同的散列码。但是对于不行等的对象产生截然不同的整数结果,有可能提高散列表的性能。
如何改写
一个好的散列函数通常倾向于“为不相等的对象产生不行等的散列码”
-
把某个非零常数值,比如17,保存在一个result的int型变量中
-
对于对象中没个“关键”域,完成以下步骤:
a. 为该域计算int类型的散列码c:- 该域为boolean类型,在计算(f ? 0 : 1)
- byte、char、short、int类型, 则计算(int)f
- long类型,则(int)(f ^ (f >>> 32))
- float类型,则Float.floatToIntBits(f)
- double类型,则Double.doubleToLongBits(f)得到一个long类型得知,然后按照long类型步骤计算
- 如果该域是对对象的引用,并且该类的equeals方法通过递归调用equals的方式类比较这个域,则同样对这个域递归调用hashCode。如果要求一个更为复杂的比较,则为这个与计算一个"规范表示",然后针对这个范式表示调用hashCode,如果阈值为null,则返回0
- 如果改于十一个数字,则把数组中的每个元素当作一个单独的与来处理
b. 把步骤a中计算的散列码c组合到result中:
result = 37 * result + c;
-
返回result
-
该写完后,检查“是否相等的实例具有相等的散列码”,如果不是,找出原因并改正。
-
当一个类是可变的,并且计算散列码的代价也比较大,这是需要考虑把散列码缓存中对象内部
摘自: 《Effective Java》