通过HashMap认识equals与hashcode

什么是hashcode,hashcode的作用是什么

hashcode并不是java中独有的。设想一下,如果让你设计一个算法,根据关键码去得到一个集合中的某个值或者这个关键码所在的位置。普通的做法就是挨个比较,高级一点的使用二分检索或者树形检索等算法。但是以上的检索算法都跟集合的长度N有关,当问题规模N很大时,这些检索的效率可能十分低下。

理想的情况是,根据关键码,我们就可以定位记录所在的位置,而不用去挨个进行比较。也就是说,在关键码与记录存放的位置之间做一种映射。这个映射的方法就是hash(哈希)函数,或者叫散列函数,也就是java中的hashCode()方法,他所返回的值就是hashcode,根据hashcode可以找到记录的位置。

按照散列的存储方式构造的存储结构叫做散列表。散列表中的一个位置称之为一个槽。

hashCode()方法存在于java.lang.Object类当中,任何类都可以继承修改这个方法。hashCode()方法返回调用它的实例的hashCode值,是个int值。

注:以下代码均来自jdk1.7

String中hashCode()方法的实现:

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
}

什么是equals(Object obj)方法

equals(Object obj)方法同样来自Object类。在Object类中,他是这样实现的:

public boolean equals(Object obj) {
        return (this == obj);
}

也就是说,默认的equals(Object obj)方法直接将要比较的两个对象的内存地址进行了比较,一致则返回true。

这个方法主要用来实现两个对象间的比较,确认他们在逻辑上是否相等。我们同样可以实现自己的equals(Object obj)方法。

String中equals(Object obj)方法的实现:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                            return false;
                    i++;
                }
                return true;
            }
        }
        return false;
}

在java中hashcode方法与equals方法的作用

首先看一下HashMap中的put方法:

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//得到hash值
        int i = indexFor(hash, table.length);//找到槽
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

我们从 int hash = hash(key); 这一行看起,这行起才是put方法的核心。

首先 int hash = hash(key); key就是我们之前提到的关键码,我们看看HashMap中的这个hash方法做了些什么:

final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

可以看到,这个方法里调用了key本身的hashCode方法,得到了key的hashcode,然后对该hashcode进行了一些移位操作,最终返回操作后的int值。返回的这个值就是HashMap要用到的hashcode值,通过他可以找到记录所在的位置。那么现在有一个问题:为什么要专门调用这个hash(Onject key)方法来对key的hashcode进行包装然后再使用呢?可以直接使用key的hashcode的呀,这样做看起来不是多此一举吗?

其实这样做的目的是为下面的函数做准备的,我们看接下来要执行的代码:

int i = indexFor(hash, table.length);找到所谓的槽,也就是记录存在的位置。

/**
  * Returns index for hash code h.
  */
static int indexFor(int h, int length) {
     // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

可以看到,indexFor(int h, int length)如何通过hashcode得到记录的位置。indexFor方法内部是一个取模运算,h是我们通过上面的hash方法得到的,length是散列表的长度。HashMap中的散列表是一个数组,通过取模运算能保证indexFor方法的返回值(记录的位置)一定在这个数组内,没有超过其长度。因为h往往是一个很大的数字(int可以表示40亿这么大的数字),而散列表的初始长度是可以由我们指定的(默认是16),另一方面,就算给他这么大的数组,内存也是放不下的。所以取模运算是必须的。经过取模运算得到的才是真正的槽值。

回到上一个问题,为什么要专门调用这个hash(Onject key)方法来对key的hashcode进行包装然后再使用呢?而不是直接使用key的hashcode的

想明白这个问题,参考JDK 源码中 HashMap 的 hash 方法原理是什么?。我们上面也说了这样做的目的是为indexFor方法做准备的,总的来说就是为了让取模运算不会出现一种极端情况:大量的不同的h经过取模后返回同样的槽值。这样会带来严重的性能问题,也就是严重的冲突情况导致性能下降。关于冲突,看下文。

要理解接下来的代码,我们就需要知道哈希算法的另一个概念:冲突。

散列函数可能对于不相等的关键码计算出相同的hashcode,该现象称为冲突。怎么理解呢?

比如我们有这样一个串abcd,我们给出这样一个散列函数:将每一个字符的ascii值加起来除以字符的个数,得到他们的平均值就是这个串的hashcode。那么,可以保证没有其他的串经过这样的算法得到相同的hashcode吗?也就是说,无限多的元素通过散列函数映射到有穷集合上,一定会产生冲突。这也是我们理解hashcode的一个重要的点:不同的对象(equals返回false)可以有相同的hashcode

那么,产生冲突怎么办呢?产生冲突之后,不同的对象在散列表中找到了相同的位置,为了解决这个问题,我们将这个槽中的内容设计成一个链表,当产生冲突的时候,就将新的元素放到链表中,他看起来是这样的:

image.png

其中:A,B,C分别为三条记录,他们就是产生冲突的三条记录。1,2,3....为散列表的索引位置。

接下来的代码 for (Entry<K,V> e = table[i]; e != null; e = e.next)就容易理解了。找到记录所对应的槽之后,遍历这个链表直到找到与关键码相同的位置(可能之前已经有以这个关键码为key的value插入)。如果遍历完链表还没有找到这样的值,说明还不存在此关键码对应的记录,直接插入即可:addEntry(hash, key, value, i);.

那么,怎么判断两个关键码在逻辑上是否相同呢?

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

可以看到,首先判断关键码的hashcode与链表记录的关键码hashcode是否相同:·e.hash == hash。为什么要加这样的判断?回头看看indexFor方法,经过取模运算后,不同的hashcode可以被散列在同一个槽中。通过这句代码可以将那些因为取模运算散列到同一个槽里的不同对象排除。

我们知道不同的对象(equals返回false)可以有相同的hashcode。相同的对象hashcode也必须相等吗?试想一下,如果两个对象相同,但是他们的hashcode不同,那么这两个对象很有可能被散列在不同的槽里,造成了同一个对象重复存储的问题。所以,我们又得出一个重要结论:相同的对象(equals返回true)hashcode一定相等

e.hash == hash && ((k = e.key) == key:这段代码首先判断hashcode是否相等,然后判断关键码是否相等。注意,是判断关键码是否相等,直接比较内存地址,如果满足以上条件,那么可以断定两个关键码相同,是我们要找的记录。

key.equals(k):如果上述两个条件没有满足,并不能够断定这两个关键码相等,此刻要使用equals方法判断这两个关键码是否相同。如果相同,说明是我们要找的记录。

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))这句代码中其实包含了一种短路思想,|| 之前的判断如果生效,那么之后的key.equals(k)就不会再执行。很明显内存地址的比较要比equals方法高效的多。这也是Hashmap提高查找效率的一个重要手段。

至此,我们应该对equals和hashcode有了一个相对清晰的认识:hashcode提高了查找指定对象的效率。euqals定义了两个对象之间是否在逻辑上相同。hashcode只在HashMap,HashSet等这样使用了散列思想的地方用到,而equals在判断两个对象之间是否相同时需要用到,比如排序等。

总结

通过上面的分析,我们知道了hashcode与equals的几个关键:

1.不同的对象(equals返回false)可以有相同的hashcode

2.相同的对象(equals返回true)hashcode一定相等

3.若重新定义了上面两种方法中的一种,那么另一种方法也需要重新定义(对1、2的遵守)

关于如何重写equals方法与hashCode方法

equals与==

"==" 比较的是两个对象的内存地址,是物理意义上的相等

equals比较的是两个对象逻辑意义上的相等,是逻辑意义上的相等

两个对象进行比较:

== 返回true,则equals一定返回true

equals返回true,== 不一定返回true

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

推荐阅读更多精彩内容

  • 实际上,HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算...
    曹振华阅读 2,495评论 1 37
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,213评论 0 16
  • 我以为躲进汹涌的人潮就能把孤独脱结 我以为借助于黑夜才敢把深埋已久的故事一一揭开 我以为一个漠然的眼神就能把之前所...
    劳心者阅读 505评论 3 11
  • 下面直接上代码 第一种替换Image图片的方式: using UnityEngine;using System.C...
    上善若水jf阅读 7,409评论 2 10