1、何时实现hashCode方法
上一篇文章介绍了如何实现equals方法如何实现equals方法,hashCode跟equals一样,都是基类Object中的一个方法。而什么时候该重写hashCode方法呢?其实这个问题的答案我们也许都知道,就是我们的类需要使用到集合框架时,绝大多数情况都要实现equals和hashCode方法,而不能只实现这两个方法其中一个。为什么呢?我们可以看一个例子:
public class Goods {
public int id;
public String goodsName;
@Override
public boolean equals(Object obj) {
if(this == obj)
return true;
if(!(obj instanceof Goods))
return false;
Goods target = (Goods)obj;
if(this.id != target.id)
return false;
if(target.goodsName == null || !target.goodsName.equals(this.goodsName))
return false;
return true;
}
public static void main(String[] args) {
Set<Goods> goodsSet = new HashSet<Goods>();
Goods goods = null;
for(int i=0;i<2;i++) {
goods = new Goods();
goods.id=1;
goods.goodsName="可口可乐";
if(!goodsSet.contains(goods)) {
goodsSet.add(goods);
}
}
System.out.println(goodsSet);
}
}
上面的Goods类只实现了equals方法,而没有实现hashCode方法,我们构造两个Goods对象,假设我们期望在业务逻辑中id和goodsName相同的被认为是同一个商品,而不是两个,但在以上的代码中连续添加“相同的”商品到一个集合里,会发现两个goods都被加进集合里!这在业务范畴来说是不可接受的。
2、Object里对equals方法实现的几个规范
基类Object在equals方法上注释了几个规范:
* <ul> * <li>Whenever it is invoked on the same object more than once during * an execution of a Java application, the {@code hashCode} method * must consistently return the same integer, provided no information * used in {@code equals} comparisons on the object is modified. * This integer need not remain consistent from one execution of an * application to another execution of the same application. * <li>If two objects are equal according to the {@code equals(Object)} * method, then calling the {@code hashCode} method on each of * the two objects must produce the same integer result. * <li>It is <em>not</em> required that if two objects are unequal * according to the {@link java.lang.Object#equals(java.lang.Object)} * method, then calling the {@code hashCode} method on each of the * two objects must produce distinct integer results. However, the * programmer should be aware that producing distinct integer results * for unequal objects may improve the performance of hash tables. * </ul>
简单的翻译并概括一下这段话的意思:
- 如果对象中equals方法所用到的信息没有被修改,那么对这个对象的多次调用hashCode方法都应该返回相同的值。在一个应用程序跟另一个应用程序执行过程中,hashCode方法可以返回不一致;
- 如果两个对象经过equals方法对比是相等的,那么调用hashCode要返回相同的值;
- 如果两个对象经过equals判定为不相等,那么调用hashCode方法不一定要求返回不同的值。但应该考虑尽可能让两个不相等的对象产生不同的hashCode,这有利于散列表的性能提高。
3、如何重写高效的hashCode
基于上面的几个理论说明,我们来考虑下如何正确并实现高效能的hashCode方法。我们都知道hashCode是一个整型值,看一下以下的hashCode重写范例。
@Override
public int hashCode() {
return 11;//不合理的散列码,会导致构造出的散列表退化成链表
}
这种hashCode的实现方法,是符合Object定义的那三条规范的。但是问题也随之而来,因为所有的对象都是返回相同的hashCode,那么构造的散列表,就会退化成链表,我们都知道链表的搜索性能是非常差的,所以这种不负责任的hashCode实现方式是不合理的,应该要考虑如何尽可能的让不同的对象产生的hashCode也不同。
那么如何快速的写出合理且高效的hashCode方法呢?很简单,按照下面这个步骤来:
- 先声明一个int变量并且命名为result,将它初始化为对象中第一个关键域(字段)的散列码。接着计算第二个关键域(字段)的散列码c,并按照公式result=result*31+c累加到result上,并以此类推到所有关键域;
- 如果关键域f是基本类型,那么它的散列码就是Type.hashCode(f),其中Type是Integer、Double这些装箱类型;
- 如果关键域是一个对象引用,那么它的散列码就是通过调用这个引用的hashCode方法得到的值,如果这个域是null,则返回0,或者返回某个常数;
- 如果关键域是一个数组,则需要遍历数组中所有的元素,并求出每个元素的散列码,并按照公式result=result*31+c把这些散列码组合到result中;
- 返回result。
这里直接上代码:
public class Goods {
public int id;
public String goodsName;
List<Integer> refGoods = new ArrayList<>();//关联的商品id列表
@Override
public boolean equals(Object obj) {
if(this == obj)
return true;
if(!(obj instanceof Goods))
return false;
Goods target = (Goods)obj;
if(this.id != target.id)
return false;
if(target.goodsName == null || !target.goodsName.equals(this.goodsName))
return false;
for(Integer i : this.refGoods) {
if(!target.refGoods.contains(i))
return false;
}
return true;
}
@Override
public int hashCode() {
int result = Integer.hashCode(this.id);
result = 31 * result + (this.goodsName == null?0:this.goodsName.hashCode());
for(Integer i : this.refGoods) {
result = 31 * result + i.hashCode();
}
return result;
}
}
此处计算散列码为什么要用31这个数字呢?我觉得这个是个约定俗成的方案,另外也是有一定的道理的。有两方面因素:
- **因为31是个“不大不小”的奇素数。**如果用偶数来作为乘数,则有可能出现乘法溢出的后果,因为乘2运算相当于移位,有可能会溢出造成数据丢失。如果这个奇素数选择得比较小,例如3,则导致计算出的散列码过于小,造成的哈希冲突比较多。如果这个奇素数选择得比较大,则有可能算出的结果超过了整型的最大值。因此一个“不大不小”的奇素数31是个不错的选择;
- 利用jvm的优化特性。因为n*31会被编译器优化成(n<<5)-n,变成移位和减法来代替的话,性能得到极大的提升。
4、提高不可变类的hashCode性能
对于不可变类,并且计算散列码的开销也很大,就应该把散列码缓存在对象内部,避免每次都重新计算散列码。
private int hashCode;
public int hashCode(){
int result = hashCode;
if(result == 0){
result = this.addresss.hashCode();
result = result*32 + Objects.hash(this.goodsList);
}
return result;
}
5、总结
总而言之,每当我们重写equals方法时,必须相应的也把hashCode方法也实现了,不然程序将无法正确运行。另外,hashCode方法也必须遵循Object类定义的那三个通用约定,实现高效的hashCode方法,这才会让程序正确并高效的跑起来!
equals方法与hashCode方法就像是不可分割的两姐妹,此处给出hashCode实现方法的姐妹篇之《如何实现equals方法》。