关于 hashCode 与 equals
Lynch Lee BIG_BOSS

Java 中,重写 equals() 必须要重写 hashCode(),那么为什么?简单来说,两个对象如果相等,则 hashCode 一定也是相同的。两个相等对象比较时,调用 equals() 会返回 true。但是由于哈希冲突,两个有着相同 hashCode 的对象也不一定相等。因此,覆盖equals()时也需要覆盖 hashCode()

Hash 冲突:Hash 算法并不完美,有可能两个不同的原始值在经过哈希运算后得到同样的结果, 这样就是哈希冲突(碰撞)。

业务逻辑

一般在创建类对应的散列表时,hashCode() 是生效的。举个例子,有这样一个业务场景(源于 ThoughtWork 暑期实习结对编程面试题):

  • 每个商品有自己的 ProductCode,以 “BULK_BUY_2_GET_1” 开头的 ProductCodeProduct.name 相同的商品享受买2送1,即三件商品付两件多的价格。
  • 购物车中可能存在各种 Product.name 的商品
  • 我们规定拥有相同名称价格的商品为同一件商品

解决方式是创建一个 Map 保存一个 相同 商品的商品数量,创建一个 Set 保存购物车中所含商品的种类。

1
2
Map<String, Integer> buy2Get1CartMap = new HashMap<>();
Set<Product> productSet = new HashSet<>();

在结算时遍历 SetMap 中取出每种商品的数量进行总价计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//...
if (product.getProductCode().startsWith("BULK_BUY_2_GET_1")) {
//更新买二减一商品的数量清单
buy2Get1CartMap.put(product.getName(), buy2Get1CartMap.getOrDefault(product.getName(), 0) + 1);
//向 Set 中添加商品,利用 Set 的特性去重。
productSet.add(product);
}
//...
totalPrice += product.getPrice() - discount;
//...
for (Product item : productSet) {
int twoGetOneSum = buy2Get1CartMap.get(item.getName()); //取得可减免商品的数量
int freeSum = twoGetOneSum / 3; //计算可剪除的商品数量
totalPrice -= freeSum * item.getPrice(); //减除可减免的商品
}

注意:这里不一定是这个业务逻辑的最佳解决方案,作者只是想借用其讲述 hashCode() 的使用场景

重写比较函数

如果你在这里实现了完整的代码会发现,代码块中的 productSet.add(product); 操作会将同名商品重复添加到 Set 中,这是怎么回事?首先他们的地址不相等这是当然的,但是他们的属性相等,至少,我们规定,同名商品就是相同的商品。这里光是针对 Product.nameProduct.price 重写 equals() 是没用的,因为不同的地址导致默认比较方法会认为他们是不同的,那么这里就需要我们改写 hashCode() 来改变比较逻辑

Product 对象中重写 equals()hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public boolean equals(Object obj) {
if (this == obj) return true; //直接返回true
if (obj == null) return false; //传入的对象为null
if (getClass() != obj.getClass()) return false; //判断两个对象对应的字节码文件是否是同一个字节码

Product other = (Product) obj; //向下转型

if (price != other.price) return false; //调用对象的价格不等于传入对象的价格
//调用对象的姓名不等于传入对象的姓名
if (name == null) return other.name == null; //调用对象的姓名为null,传入对象的姓名不为null
else return name.equals(other.name);
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
long priceToLongBit = Double.doubleToLongBits(price);
result = prime * result + (int)(priceToLongBit ^ (priceToLongBit >>> 32));
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}

equals() 中我们将判断逻辑变成了比较 Product.nameProduct.price 属性,这里为了保证比较函数的健壮性,需要对空对象进行特判,具体逻辑在备注中说的很清楚了,就不再赘述了。

重点在于 hashCode() 的改写,变量 prime 保存的是一个作为特征值的奇素数,其用于计算时带来更好的性能(在 Effective Java 48 页,第 3 章中有提到)。另外我们利用两个关键属性,即 Product.nameProduct.price 计算 hashCode, 算法具体比较复杂,我们这里简单理解为:

对每一个关键域进行计算:$reslut = prime * result + KeyHashCode$

其中 result 初始为一个非零常数,对于关键属性(称之为关键域,即公式中的 KeyHashCode)的 hashCode 计算规则如下:

类型 计算方式
byte, char, short, int (int)obj
boolean obj ? 1 : 0
long (int)(obj^(obj>>>32))
float Float.floatToIntBits(obj)
double Double.doubleToLongBits(obj) 再使用 long 的方式计算
对象引用 (obj == null) ? 0 : obj.hashCode()
Array 对每个元素单独处理 或者 使用 Arrays.hashCode()

由此我们重写了 Product 的比较函数,这里再使用 Set 去重是,就能按照业务逻辑正常工作量。

注意:作者对于 hashCode() 的重写说不上熟练,只是通过这个实例正确实现了重写的效果,实际操作中如有问题烦请及时评论。

你,学废了吗?

  • Post title:关于 hashCode 与 equals
  • Post author:Lynch Lee
  • Create time:2021-04-23 20:06:59
  • Post link:https://neeomaclynch.github.io/2021/04/23/关于-hashCode-与-equals/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments