重写 equals() 首先明确你的目的
首先我强调一下,没事别去重写eqauls()方法,除非真的需要。
很多时候,我们需要判断两个对象是否相等,比如说一个等腰三角形类:
triangle
public class Triangle{
private double A; //底边长
private double height;
private double area;
............
}
假设我们认为两个面积相等的等腰三角形,那么这两个等腰三角形就是同一个对象。
那么这时候我们就需要去重写它的equals()
方法了 。
equals约定
重写equals方法 要遵循以下几点:
- 自反性(reflexive)。对于任意不为null的引用值x,x.equals(x)一定是true。
- 对称性(symmetric)。对于任意不为null的引用值x和y,当且仅当x.equals(y)是true时,y.equals(x)也是true。
- 传递性(transitive)。对于任意不为null的引用值x、y和z,如果x.equals(y)是true,同时y.equals(z)是true,那么x.equals(z)一定是true。
- 一致性(consistent)。对于任意不为null的引用值x和y,如果用于equals比较的对象信息没有被修改的话,多次调用时x.equals(y)要么一致地返回true要么一致地返回false。
- 对于任意不为null的引用值x,x.equals(null)返回false。
我们很多时候可能就是直接这么写的:
@Override
public boolean equals(Object anObject){
if (this == anObject) {
return true;
}
if (anObject instanceof Triangle) {
Triangle anotherTriangle = (Triangle)anObject;
return Double.compare(this.area,anotherTriangle.area)==0;
}
return false;
}
这么看来,equals这个方法本身并没有什么问题。但是如果放在hashSet里它就有问题了。
比如以下情况:
Triangle t1 = new Triangle("6","4","12");
Triangle t2 = new Triangle("6","4","12");
Set<Triangle> tempSet = new HashSet()<Triangle>;
tempSet.add(t1);
tempSet.add(t2);
原因
HashSet
集合是不会出现重复的。对于t1和t2来说,在我们看来他们应该是同一个三角形。但是结果肯定tempSet
里会存在t1和t2。
这是因为在HashSet
里它add
方法是这样的
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
然后看map的put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
看hash方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在放入key的时候调用了hash()
方法,也就是说用的Triangle的hash方法,Triangle的hash方法继承自Object。表面上HashSet 是没有key,但从代码可以看出,他是把传入的数据hash后作为key的。Object的hash方法是native方法,不同对象返回的hash值不同。刚刚传入的两个对象实际上是地址不同的对象,所以它们的hash值肯定是不同的。所以如果我们只要重写hashCode
方法,针对相同面积的三角形,返回相同的值就行。
@Override
public int hashCode(){
int result = Double.hashCode(area);
result = 31 * result + Double.hashCode(A);
result = 31 * result + Double.hashCode(height);
return result;
}
这里用31的原因是因为31在编译器优化时会用位移和减法代替乘法可以得到更好的性能,Objects 类有一个静态方法可以提供同样的功能,但是效率稍差。原因是它会创建数组。在性能要求不高的情况下,可以用它,很方便:
@Override
public int hashCode(){
return Objects.hash(area,A,height);
}
可以看下Objects方法里实现:
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
不过因为例子的原因,其实这上面我刚刚的写法可能是有问题的。Effective Java中说过
如果一个域的值可以根据参与计算的其他域值计算出来,则可以把这个域排除在外。
我这里关键域是area,但是它确实可以通过底边和高求出。但千万不要为了效率而排出关键域(就是eqauls对比的字段)。不然肯定会出乱子。
上面可能会违反hashCode约定的第二条。这里只是为了方便理解。还希望不要效仿。。。
- 在应用程序中,只要对象的euqals方法的比较操作所用的信息没有修改,那么对于同一个对象的调用多次hashCode,必须始终如一返回同一个哈希值。
- 如果两个对象通过equals比较相等,那么它们的哈希值相同。
- 如果两个对象通过euqals比较不等,他们的哈希值可能相同,取决于hashCode的实现,由此散列表的性能也会有区别。
其实这也再一次告诉我们,重写equals方法时,必须要重写hashCode方法。
这里推荐AutoValue ,它可以帮你自动生成hashCode方法。部分IDE也支持自动生成。