今天在调用别人写的对象时发现一个很奇怪的问题,首先描述下需求:

简要需求:从数据库查询一个对象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不一定不一样;

好吧,这不是口诀,这完全是文言文,希望大家在理解的基础上去记忆,否则光靠背,这东西时间久了就迷糊了。