Java基础——HashCode与equals

1、hashCode()介绍:

        hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数。这个哈希码的作⽤是确定该对象在哈希表中的索引位置。 hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode ⽅法是native本地⽅法,也就是⽤ c 语⾔或 c++ 实现的,该⽅法通常⽤来将对象的 内存地址 转换为整数之后返回。

2、为什么要有 hashCode?

        当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同时也会与其他已经加⼊的对象的 hashcode 值作⽐较,如果没有相符的 hashcode, HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让其加⼊操作成功。如果不同的话,就会重新散列到其他位置。这样我们就⼤⼤减少了 equals 的次数,相应就⼤⼤提⾼了执⾏速度。

默认HashCode方法:

	public static int hashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());

        return result;
    }

        以字符串"123"为例:字符'1'的ascii码是49,hashCode = (49 * 31 + 50)* 31 + 51或者这样看:hashCode=('1' * 31 + '2' ) * 31 + '3'可见实际可以看作是一种权重的算法,在前面的字符的权重大。这样有个明显的好处,就是前缀相同的字符串的hash值都落在邻近的区间

好处有两点:

  1. 可以节省内存,因为hash值在相邻,这样hash的数组可以比较小。比如当用HashMap,以String为key时。
  2. hash值相邻,如果存放在容器,比好HashSet,HashMap中时,实际存放的内存的位置也相邻,则存取的效率也高。(程序局部性原理)以31为倍数,原因了31的二进制全 1,则可以有效地离散数据

为什么使用 31

        是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5)- i 移位计算比乘法快很多。

3、如何解决hash冲突

解决哈希冲突的四种方法(拉链法、开放地址法、再散列法、建立公共溢出区

  • 拉链法:HashMap,HashSet其实都是采用的拉链法来解决哈希冲突的,就是在每个位桶实现的时候,我们采用链表(jdk1.8之后采用链表+红黑树)的数据结构来去存取发生哈希冲突的输入域的关键字(也就是被哈希函数映射到同一个位桶上的关键字)。
  • 开放地址法:开放地址法有个非常关键的特征,所有输入的元素全部存放在哈希表里。位桶的实现是不需要任何的链表来实现的,换句话说,也就是这个哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列一直往后查找),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。
  • 再散列法:再散列法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次散列,直至不发生冲突位置。    缺点:每次冲突都要重新散列,计算时间增加。
  • 建立公共溢出区:建立公共的溢出区,存放所有hash冲突的数据。

4、HashMap扩容

        创建HashMap对象默认情况下,数组大小为16。 开始扩容的大小=原来的数组大小*loadFactor。 扩容后大小是原来的2倍,其中加载因子loadFactor的默认值为0.75,这个参数可以再创建对象时在构造方法中指定。

        16 * 0.75=12,默认创建一个map对象数组大小是16,当map添加12个元素到的时候就发生扩容,创建新的数组的大小 2 * 16=32,然后重新计算每个元素在新数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

扩容以后需要重新更新之前HashMap中的元素:

        其中图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,n代表length:

        元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

        当超出阈值后,数组扩容为原来的2倍,而JDK1.7的实现那样重新计算hash。1.8以后只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图(一方面位运算更快,另一方面抗碰撞的Hash函数其实挺耗时的)

源码:

//判断的hash值新增的那个bit是1还是0
do{
    next = e.next;
    // e.hash 为 此时的hash值...00101;oldCap 为之前的数组长度(2的n次幂)16...10000;为0则插入原索引
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    
    // 此时的hash值...10101;oldCap 为之前的数组长度(2的n次幂)16...10000;为1原索引+oldCap
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);

 HashMap 的容量为什么建议是 2的幂次方?

        如果Map中已有数据的容量达到了初始容量的 75%加载因子loadFactor的默认值为0.75),那么散列表就会扩容,而扩容将会重新将所有的数据重新散列,性能损失严重。所以,我们可以必须要大于我们预计数据量的 1.34 倍,如果是2个数据的话,就需要初始化 2.68 个容量。2.68 不可以,3 可不可以呢?肯定也是不可以的,我前面说了,如果不是2的幂次方,散列结果将会大大下降。导致出现大量链表。那么我可以将初始化容量设置为4。 当然了,如果你预计大概会插入 12 条数据的话,那么初始容量为16简直是完美,一点不浪费,而且也不会扩容。容量是2的幂次方,则可以使用上面的&(与运算)计算hash值。

HashMap 为什么使用 & (与运算)代替模运算?

        tab[(n - 1) & hash]:其中 n 是数组的长度。其实该算法的结果和模运算的结果是相同的。

        a % n == a & (n-1),当n是2的指数时,等式成立。:我们说 & (与运算)的定义:与运算 第一个操作数的第n位与第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0;

eg:a = 9 -> 1001

        n = 8 -> 1000 ;

        n - 1 = 7 -> 0111

a%n = 1 a&(n-1) = 0001 = 1 a % n == a & (n-1)

5、为什么重写 equals 时必须重写 hashCode ⽅法?

        HashCode()方法的默认行为是对堆上的对象产生独特值,如果没有重写HashCode()方法,则该class的两个对象无论如何都不会相等(即时这两个对象有相同的数据)。

1、重写hashcode是为了保证相同的对象会有相同的hashcode值

2、重写equals是为了保证在发生冲突的情况下取得到Entry对象(也可以理解是key或是元素)

3、同时重写hashcode和equals方法可以迅速的在hashmap中找到键的位置

        如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青山孤客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值