equals方法的姐妹篇——如何实现高效的hashCode方法

当类需要使用到集合框架时,需要同时实现equals和hashCode方法。本文详细讲解了何时及如何实现hashCode方法,包括Object类中equals方法的规范、重写hashCode方法的原则和步骤,以及如何提高不可变类的hashCode性能。强调了hashCode方法对于散列表性能的重要性,并提供了实现示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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方法呢?很简单,按照下面这个步骤来:

  1. 先声明一个int变量并且命名为result,将它初始化为对象中第一个关键域(字段)的散列码。接着计算第二个关键域(字段)的散列码c,并按照公式result=result*31+c累加到result上,并以此类推到所有关键域;
  2. 如果关键域f是基本类型,那么它的散列码就是Type.hashCode(f),其中Type是Integer、Double这些装箱类型;
  3. 如果关键域是一个对象引用,那么它的散列码就是通过调用这个引用的hashCode方法得到的值,如果这个域是null,则返回0,或者返回某个常数;
  4. 如果关键域是一个数组,则需要遍历数组中所有的元素,并求出每个元素的散列码,并按照公式result=result*31+c把这些散列码组合到result中;
  5. 返回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方法》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值