所有类都继承自Object类,他所有的非final方法:equals,hashCode, toString, clone 和 finalize,它们都有通用约定。 我们在覆盖这些方法的时候需要遵循这些约定,否则依赖这些约定的类(例如HashMap和HashSet)就无法结合该类一起工作了。
如果不重载equals函数,那么两个类的相等只能是真正意义上的equal。如果类想要自己的相等逻辑就需要像Integer/List那样重载equals函数。
下面可以通过两个不同的情况看待这个问题:
如果子类能够拥有自己的相等概念, 则对称性需求强制采用getClass进行检测 如果由超类决定相等的概念, 那么就用instanceof进行检测,这样可以在不用子类的对象之间进行相等的比较
TimeStamp的不对称性
Date date = new Date(); Timestamp t1 = new Timestamp(date.getTime()); System.out.println("Date equals Timestamp ? : " + date.equals(t1));// true System.out.println("Timestamp equals Date ? : " + t1.equals(date));// false
TimeStamp源码:(使用了instanceof 而不是 getClass())
// Timestamp @Override public boolean equals(java.lang.Object ts) { if (ts instanceof Timestamp) { return this.equals((Timestamp)ts); } else { return false;// 非Timestamp 实例直接返回false } } // 省略其他代码 public boolean equals(Timestamp ts) { if (super.equals(ts)) { if (nanos == ts.nanos) { return true; } else { return false; } } else { return false; } }
父类Date:
// Date @Override public boolean equals(Object obj) { return obj instanceof Date && getTime() == ((Date) obj).getTime(); }
public boolean equals(Object otherObject)
if(this==otherObject){
return true;
}
if(otherObject == null){
return false;
}
if(getClass() != otherObject.getClass()){
return false;
}
如果所以子类语义相同,使用instanceof检测:
if(!(otherObject instanceof Employee)){
return false;
}
Employee other = (Employee)otherObject;
return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
设计原则中有一条: 覆盖equals时总要覆盖hashCode
1.只要对象equals方法的比较操作所用到的信息没有被修改,对同一对象调用多次,hashCode方法都必须返回同一整数。在同一应用程序的多次执行过程中,每次执行返回的整数可以不一致。
2.如果两个对象根据equals(Object)方法比较是相等的,那么这两个对象的hashCode返回值相同。
3.如果两个对象根据equals(Object)方法比较是不等的,那么这两个对象的hashCode返回值不一定不等,但是给不同的对象产生截然不同的整数结果,能提高散列表的性能。
如果一个类覆盖了equals覆盖了equals函数,却没有覆盖hashCode会违反上述第二条原则。下面看一下没有重载hashCode的例子:
public class PhoneNumber { private final int areaCode; private final int prefix; private final int lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { rangeCheck(areaCode, 999, "area code"); rangeCheck(prefix, 999, "prefix"); rangeCheck(lineNumber, 9999, "line number"); this.areaCode = areaCode; this.prefix = prefix; this.lineNumber = lineNumber; } private static void rangeCheck(int arg, int max, String name) { if(arg < 0 || arg > max) { throw new IllegalArgumentException(name + ": " + arg); } } @Override public boolean equals(Object o) { if(o == this) return true; if(!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber)o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; } }
执行如下代码:
Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>(); map.put(new PhoneNumber(707, 867, 5309), "Jenny"); System.out.println(map.get(new PhoneNumber(707, 867, 5309)));
我们期望它返回Jenny,然而它返回的是null。
原因在于违反了hashCode的约定,由于PhoneNumber没有覆盖hashCode方法,导致两个相等的实例拥有不相等的散列码,put方法把电话号码对象放在一个散列桶中,get方法从另外一个散列桶中查找这个电话号码的所有者,显然是无法找到的。
只要覆盖hashCode并遵守约定,就能修正这个问题。
一个好的散列函数倾向于“为不相等的对象产生不相等的散列码”,下面有简单的解决办法:
1.把某个非零的常数值,如17,保存在一个名为result的int类型的变量中。(为了2.a中计算的散列值为0的初始域会影响到散列值)
2.对于对象中的每个关键域f,完成一下步骤:
a.为该域计算int类型的散列码c
i.如果该域是boolean,计算(f ? 1:0)
ii.如果该域是byte、char、short或者int类型,则计算(int)f
iii.如果该域是long,则计算(int)(f ^ (f >>> 32))
iv.如果该域是float,则计算Float.floatToIntBits(f)
v.如果该域是double,则计算Double.doubleToLongBits(f),然后
vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”,然后针对这个“范式”调用hashCode。如果域的值为null,则返回0(或其他某个常数,但通常为0)。
vii.如果该域是一个数组,则要吧每一个元素当做单独的域来处理,也就是要递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据2.b把这些散列值组合起来。如果数组域中的每个元素都很重要,可以使用1.5中增加的其中一个Array.hashCode方法。
b.按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:
result = 31 * result + c。(选择31是因为它是一个奇素数,如果乘数是偶数,乘法溢出时会丢失信息,VM可以优化 31 * i == (i << 5) - i)
3.返回result。
编写完hashCode方法后,编写单元测试来验证相同的实例是否有相等的散列码。
把上面的解决方法应用到PhoneNumber类中:
@Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; return result; }
现在使用之前的测试代码,发现能够返回Jenny了。
原文:https://www.cnblogs.com/NeilZhang/p/10963516.html