为什么重写equals就必须重写hashcode?
我们必须先了解hash桶。水桶用来存放水,而hash桶用来存放多个hash值,hash算法负责将hash值分配到hash桶里,而相同的hash值始终位于相同的桶内。当存储元素时首先计算出hash值,然后找到对应的hash桶,把值放在该桶里。查找元素时,同样计算出hash值并找到对应的hash桶,随后将桶内所有元素取出一一对比equals,即完成查找。
为什么要利用hash桶呢?这样是为了减少需要用equals比较的元素数量。想象下传统的方案中我们将所有元素一一对比equals,而现在只需和一个hash桶内元素对比即可。
那如果不重写hashcode,即可能出现equals相同的元素其hashcode不同。假如我们已经在set中插入了一个元素,现在插入一个跟它equals的元素,但由于两者hashcode不同,因此被分配到不同的桶中,导致这个元素被添加进set,这违反了set的规则!
从上述可以看出,hashcode的作用是辅助equals的判断,它主要考虑的是效率。
重写equals
重写equals具有许多陷阱,稍不小心就会违反下面这些原则:
- 自反性
- 相对性
- 传递性
- 永久性
- 任何非NULL对象不等于NULL
接下来我们先给出一个标准实现,然后一一解释
@Override
public boolean equals(Object o) {
//自检查
if(this == o )
return true;
//空检查
if (o == null)
return false;
//类型检查和转换
if (getClass() != o.getClass())
return false;
Car car = (Car)o;
// 成员比较
return Objects.equals(name, car.name);
}
- 函数参数
函数参数一定要为object,考虑如下情况:
Car a = new Car("my");
Object b = new Car ("my");
boolean answer = a.equals(b);
这里answer将为false。因此java的方法重载基于静态类型,因此这里调用的是Obeject.equals方法。当我们使用集合时会大量遇到这种情况。
自校验
equals是一个被经常调用的基本方法,因此需要注意它的性能,自校验可以极大的提高性能。空校验
空校验保证上述最后一条原则,也避免了NullPointerException类型检查和转换
这里有两种实现方式,我们采取的方式将导致所以子类equals返回false,这可能不是我们想要的结果,因为有些子类仅添加功能而不增加成员变量,那么它们应该允许和父类相等。对hibernate或者spring这些会在运行中产生子类的框架来讲这点尤为重要。因此,我们衍生出另一种做法采用instanceofif (!(o instanceof Car)) return false;
但需要注意的是这种做法会引起其他问题。例如SUV派生自Car同时添加了新的成员变量并覆盖equals方法,那么car.equals(suv)将返回true,而suv.equals(car)将返回false,这显然违反了相对性。
当然我们可以在suv的equals中检查object是否为car来避免这个问题,但这种做法违反了传递性,比如:Car a = new Car("my"); SUV s1 = new SUV("my", "1"); SUV s2 = new SUV("my", "2");
a分别和s1以及s2满足equals,但s1和s2不满足。
所以这里有两种做法:使用getclass同时记住所有的子类和父类不能equals;使用instanceof方法同时设置equals为final成员比较
基本类型使用==,引用类型使用equals,对于可能为空的字段需要检验是否为NULL。因此建议采用Objects.equals方法重写hashcode
重写hashcode需要注意几点:
选择字段
不能选择equals函数没有采用的字段,这样可能导致两个equals的对象其hashcode却不相同!换句话即采用equals字段的子集。一致性
如果不小心将已经添加到hashset的对象作出了修改,由于hash值不会重新计算,hashset的内部结构也不会改变,因此接下来即使使用相equals的对象也无法查找到该元素!即使使用该元素本身也不行!所以我么需要尽量使用不可变变量来计算hash值。计算过程
把某个非0的常数值,比如17,保存在一个名为result的int类型的变量中。
对于对象中的每个域,做如下操作:
为该域计算int类型的哈希值c:
如果该域是boolean类型,则计算(f?1:0)
如果该域是byte、char、short或者int类型,则计算(int)f
如果该域是long类型,则计算(int)(f^(f>>>32))
如果该域是float类型,则计算Float.floatToIntBits(f)
如果该域是double类型,则计算Double.doubleToLongBits(f),然后重复第三个步骤。
如果该域是一个对象引用,并且该类的equals方法通过递归调用equals方法来比较这个域,同样为这个域递归的调用hashCode,如果这个域为null,则返回0。
如果该域是数组,则要把每一个元素当作单独的域来处理,递归的运用上述规则,如果数组域中的每个元素都很重要,那么可以使用Arrays.hashCode方法。
把上面计算得到的hash值c合并到result中