覆盖equals时总要覆盖hashCode

1. 什么是hashcode方法?
  • hashcode方法返回对象的哈希码值
  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有改变,那么对于这同一个对象调用多次,hashcode方法都必须返回同一个整数。
  • hashcode的存在主要用于查找的快捷性,如Hashtable,HashMap等,hashcode是用来在散列存储结构中确定对象的存储地址的。
2. hashcode相等与对象相等之间的关系:(保证设计是规范的前提下)
  • 如果两个对象相同,那么两个对象的hashcode也必须相同
  • 如果两个对象的hashcode相同,并不一定表示两个对象就相同,也就是不一定适合equals方法,只能够说明两个对象在散列表存储结构中,“存放在同一个篮子里”。
3. 为什么覆盖equals方法时总要覆盖hashcode方法?

因为如果不覆盖equals方法的话,相等的对象可能返回的不相同的hash code。

例子:我们创建两个相同值的对象,然后将p1对象作为key,"jenny"插入到hashmap中,再将p2对象作为key,获取对应的value值

public class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }
    //覆盖equals方法
    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof PhoneNumber))
            return false;
        //必须满足如下条件,才能说明为同一个对象
        PhoneNumber pn = (PhoneNumber) obj;
        return pn.areaCode == areaCode && pn.prefix == prefix && pn.lineNumber == lineNumber;
    }

    public static void main(String[] args){
        Map<PhoneNumber, String> m = new HashMap<>();
        //创建两个相同的对象
        PhoneNumber p1 = new PhoneNumber(707, 867, 5309);
        PhoneNumber p2 = new PhoneNumber(707, 867, 5309);
        //添加到hashmap中
        m.put(p1, "Jenny");
        //比较对象p1和p2
        System.out.println("p1.equals(p2): " + p1.equals(p2));
        System.out.println("p2.equals(p1): " + p2.equals(p1));
        //从hashmap中去获取对象p1和p2
        System.out.println("get p1 from hashmap: " + m.get(p1));
        System.out.println("get p2 from hashmap: " + m.get(p2));
    }
}
输出结果:
p1.equals(p2): true
p2.equals(p1): true
get p1 from hashmap: Jenny
get p2 from hashmap: null

前两个结果说明p1和p2为同一对象,但我们发现以p1作为key时,是可以获取到对应的value,而以p2作为key时,却获取到null。

实际上,是因为PhoneNumber类没有覆盖hashcode方法,从而导致两个相等的实例具有不相等的散列码。

//输出p1和p2的hashcode
System.out.println("p1's hashcode: " + p1.hashCode());
System.out.println("p2's hashcode: " + p2.hashCode());
输出结果:
p1's hashcode: 460141958
p2's hashcode: 1163157884

因此,导致put方法时把电话存放在一个散列桶中,而get方法却在另个散列桶中查找这个电话号码。即使这两个实例正好放到同一个散列桶中(发生“冲突”的情况),get方法也必定会返回null,因为hashmap有一项优化,可以将与每个相关联的散列码缓存起来,如果散列码不匹配,那么将会返回null。

4. 如何在覆盖equals方法时覆盖hashcode方法?

实际上,问题很简单,只要我们重写hashcode方法,返回一个适当的hash code即可。

@Override
public int hashCode() {
    return 42;
}

这样的确能解决上面的问题,但实际上,这么做,会导致很差的性能,因为它总是确保每个对象都具有同样的散列码。因此,每个对象都被映射到同一个散列桶中,使得散列表退化成链表。

** 一个好的散列函数通常倾向于“为不相等的对象产生不同的散列码”**。
理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上。但实际上,要达到这种理想的情形是非常困难的。

5. 如何设置一个好的散列函数?

a. 为对象计算int类型的散列码c:

  • 对于boolean类型,计算(f?1:0)
  • 对于byte,char,short,int类型,则计算(int)f
  • 对于long类型,计算(int)(f^(f>>>32))
  • 对于float类型,计算Float.floatToIntBits(f)
  • 对于Double类型,计算Double.doubleToLongBits(f),然后再按照long类型处理
  • 对于对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashcode
  • 对于数组,则把每一个元素当作单独的域来处理。

b. 将获取到的c合并:result = 31 * reuslt + c;

c. 返回result

比如,我们可以优化上面的hashcode方法:

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

如果一个类是不可变类,并且计算散列码的开销也比较大,就应该考虑吧散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。

 private volatile static int hashcode;
    
    @Override
    public int hashCode() {
        int result = hashcode;
        if (result == 0){
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumber;
            hashcode = result;
        }
        return result;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容