今天在调用别人写的对象时发现一个很奇怪的问题,首先描述下需求:
简要需求:从数据库查询一个对象Dish,将这个Dish上传到某某平台,过一点时间后,某某平台会将该Dish对象传回来,我们需要判断这个对象在数据库是否有被修改的痕迹;我们首先想到的就是第一次从数据库查询出这个对象的时候,计算出其hashcode值一并传给某某平台,然后当某某平台将这个hashcode再次传回来的时候,我们再从数据库查询同样的对象Dish并计算出其hashcode1,如果hashcode= hashcode1,我们认为该对象在此期间在数据库中没有被修改;
需求不重要,简单说下即可,我们主要看怎么重写Dish对象的hashcode的方法的:
Dish类结构:
import java.util.List;
public class Dish {
private String dishCode;
private boolean weighing;
private String categoryCode;
private boolean currentPrice;
private boolean discountable;
private boolean stopSell;
private boolean soldOut;
private boolean deal = false;
private List<DealGroup> dealGroupList;
public String getDishCode() {
return dishCode;
}
public void setDishCode(String dishCode) {
this.dishCode = dishCode;
}
public boolean isWeighing() {
return weighing;
}
public void setWeighing(boolean weighing) {
this.weighing = weighing;
}
public String getCategoryCode() {
return categoryCode;
}
public void setCategoryCode(String categoryCode) {
this.categoryCode = categoryCode;
}
public boolean isCurrentPrice() {
return currentPrice;
}
public void setCurrentPrice(boolean currentPrice) {
this.currentPrice = currentPrice;
}
public boolean isDiscountable() {
return discountable;
}
public void setDiscountable(boolean discountable) {
this.discountable = discountable;
}
public boolean isStopSell() {
return stopSell;
}
public void setStopSell(boolean stopSell) {
this.stopSell = stopSell;
}
public boolean isSoldOut() {
return soldOut;
}
public void setSoldOut(boolean soldOut) {
this.soldOut = soldOut;
}
public boolean isDeal() {
return deal;
}
public void setDeal(boolean deal) {
this.deal = deal;
}
public List<DealGroup> getDealGroupList() {
return dealGroupList;
}
public void setDealGroupList(List<DealGroup> dealGroupList) {
this.dealGroupList = dealGroupList;
}
@Override
public int hashCode() {
int result = dishCode != null ? dishCode.hashCode() : 0;
result = 31 * result + (soldOut ? 1 : 0);
result = 31 * result + (stopSell ? 1 : 0);
result = 31 * result + (currentPrice ? 1 : 0);
result = 31 * result + (weighing ? 1 : 0);
result = 31 * result + (categoryCode != null ? categoryCode.hashCode() : 0);
result = 31 * result + (dealGroupList != null ? dealGroupList.hashCode() : 0);
return result;
}
}
其重写了hashcode方法,我们知道如果重写hashcode方法是用到了对象类型,那么该对象类型也必须要重写hashcode方法,否则每次得到的hashcode值不一定一致,那么重写hashcode方法的意义就不大了;我们发现这里面用到了dealGroupList这个对象,所有我们必须要重写这个对象里面元素的hashcode方法,即
DealGroup对象:
DealGroup类结构:
import java.math.BigDecimal;
public class DealGroup {
/**
* 是否可选 (必填) 枚举类型
*/
private OptionalTypeEnum optionalType;
private boolean repeatable;
private BigDecimal minChooseNum;
private BigDecimal maxChooseNum;
private boolean hasExtraPrice = false;
public OptionalTypeEnum getOptionalType() {
return optionalType;
}
public void setOptionalType(OptionalTypeEnum optionalType) {
this.optionalType = optionalType;
}
public boolean isRepeatable() {
return repeatable;
}
public void setRepeatable(boolean repeatable) {
this.repeatable = repeatable;
}
public BigDecimal getMinChooseNum() {
return minChooseNum;
}
public void setMinChooseNum(BigDecimal minChooseNum) {
this.minChooseNum = minChooseNum;
}
public BigDecimal getMaxChooseNum() {
return maxChooseNum;
}
public void setMaxChooseNum(BigDecimal maxChooseNum) {
this.maxChooseNum = maxChooseNum;
}
public boolean isHasExtraPrice() {
return hasExtraPrice;
}
public void setHasExtraPrice(boolean hasExtraPrice) {
this.hasExtraPrice = hasExtraPrice;
}
@Override
public int hashCode() {
int result = optionalType != null ? optionalType.hashCode() : 0;
result = 31 * result + (minChooseNum != null ? minChooseNum.hashCode() : 0);
result = 31 * result + (maxChooseNum != null ? maxChooseNum.hashCode() : 0);
return result;
}
}
到这里,对象写完了,开始描述问题。
问题描绘:查询同样的一个对象,发现其hashcode值有时候不一致,分析重写的hashcode方法,发现可能是DealGroup对象重写hashcode方法时用的的枚举类型optionalType有问题,因为枚举是个对象,且这个枚举类没有重写hashcode方法(我把hashcode方法里面的optionalType去掉就ok了);然后我就去找对应的开发,让他看看这个问题,他感觉可能是有问题,就本地测试了几遍,居然发现他这边没有问题,即他得到的hashcode值,永远都是一样,不像我有时候不一样,这我就郁闷了。
最终结论:
猜测结论,只是猜测,也希望大家能去帮我验证:我用的jdk7,同事用的jdk8,其他我俩的环境完全一样,所以猜测是这个原因,可能java8在这块做了某些修改。
好吧,我承认这个例子没啥意义,我们聊聊如何重写hashcode方法;
1、String对象和Bigdecimal对象已经重写了hashcode方法,这些类型的值可以直接用于重写hashcode方法;
2、result = 31 *result + (dishCode !=null ?dishCode.hashCode() : 0);,这里面为啥用个31来计算,而且很多人都是这么写的,这是因为31是个神奇的数字,任何数n*31都可以被jvm优化为(n<<5)-n,移位和减法的操作效率比乘法的操作效率高很多?
这边再拷贝下别人说的比较经典的总结:
Google首席Java架构师Joshua Bloch在他的著作《Effective Java》中提出了一种简单通用的hashCode算法
1. 初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;
2. 选取equals方法中用于比较的所有域,然后针对每个域的属性进行计算:
(1) 如果是boolean值,则计算f ?
(2) 如果是byte\char\short\int,则计算(int)f
(3) 如果是long值,则计算(int)(f ^ (f >>> 32))
(4) 如果是float值,则计算Float.floatToIntBits(f)
(5) 如果是double值,则计算Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int
(6)如果是对象应用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。 否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode值为0
(7)如果是数组,那么需要为每个元素当做单独的域来处理。如果你使用的是1.5及以上版本的JDK,那么没必要自己去 重新遍历一遍数组,java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上,
关于hashcode,我们一定要知道一个口诀:
1、hashcode相等,两个对象不一定相等,需要通过equals方法进一步判断;
2、hashcode不相等,两个对象一定不相等;
3、equals方法为true,则hashcode肯定一样;
4、equals方法为false,则hashcode不一定不一样;
好吧,这不是口诀,这完全是文言文,希望大家在理解的基础上去记忆,否则光靠背,这东西时间久了就迷糊了。