JAVA基础小细节——equals()与hashCode()

2022年8月17日11:17:43

Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这两个方法,今天就来介绍一些这两个方法的作用。

equals()和hashCode()方法是用来在同一类中做比较用的,尤其是在容器里如set存放同一类对象时用来判断放入的对象是否重复。

这里我们首先要明白一个问题:

equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。换句话说,equals()方法不相等的两个对象,hashCode()有可能相等。(我的理解是由于哈希码在生成的时候产生冲突造成的)

在这里hashCode就好比字典里每个字的索引,equals()好比比较的是字典里同一个字下的不同词语。就好像在字典里查“自”这个字下的两个词语“自己”、“自发”,如果用equals()判断查询的词语相等那么就是同一个词语,比如equals()比较的两个词语都是“自己”,那么此时hashCode()方法得到的值也肯定相等;如果用equals()方法比较的是“自己”和“自发”这两个词语,那么得到结果是不想等,但是这两个词都属于“自”这个字下的词语所以在查索引时相同,即:hashCode()相同。如果用equals()比较的是“自己”和“他们”这两个词语的话那么得到的结果也是不同的,此时hashCode() 得到也是不同的。

反过来:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object类中,hashcode()方法是本地方法,返回的是对象的地址值:

publicnativeinthashCode();

而object类中的equals()方法比较的也是两个对象的地址值,如果equals()相等,说明两个对象地址值也相等,当然hashcode()也就相等了。

equals()与==

1)对于==

  • 如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等
  • 如果作用于引用类型的变量,则比较的是所指向的对象的地址

2)对于equals方法(注意:equals方法不能作用于基本数据类型的变量)

  • 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址,诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容

其主要的不同是一个是操作符一个是方法,==用于对比原生类型而equals()方法比较对象的相等性。

hashCode()函数

Java 里面当你重写 equals 的时候需要同时重写 hashCode。但是如何实现一个 hashCode?

  • 返回一个固定值。
@OverridepublicinthashCode() {return42;
}

42 可是生命、宇宙以及任何事情的终极答案。 来当一个对象的hashCode 是绰绰有余了。不过虽然返回固定值是合法的行为。但是是最不推荐的一种的做法,因为这样会导致当该对象被用作 HashMap 的 Key 的时候不断的冲突get 的复杂度会退化为 O(n).所以 42 虽然是生命、宇宙以及任何事情的终极答案, 但是计算42的真实含义需要地球运算数万年才能得出。普通的电脑还是不能这样随便用的。

传统的实现方式

传统实现一个hashCode 需要三步

  • 取一个非0的 int 作为初始值,比如 42,保存在 result 中
  • 计算你所关心的域(在equals起作用的)的 hashCode。如何生成属性的 hashCode 我们等一下再说,先假设生成的 hashCode 为 c
  • 组合所有的域,result = 31 * result + c

Java 7+ 实现风格

Objects 是 Java 7 新加的一个类。其中有一个计算hash 的方法:

publicstaticinthash(Object... values)

所以计算 HashCode 只需要用下面的代码:

@OverridepublicinthashCode() {
    reutrn Objects.hash(p1, p2, p3, p4);
}

如何计算各个属性的 hash 值

这一部分我觉得用代码来说明是最简单的,代码由 IntelliJ 自动生成。主要是各个原始类型的生成规则。

publicclassTestClass {byte b;char c;short s;int i;long l;float f;double d;
    String string;@Overridepublicbooleanequals(Object o) {if (this == o)returntrue;if (o ==null || getClass() != o.getClass())returnfalse;

        TestClass testClass = (TestClass) o;if (b != testClass.b)returnfalse;if (c != testClass.c)returnfalse;if (s != testClass.s)returnfalse;if (i != testClass.i)returnfalse;if (l != testClass.l)returnfalse;if (Float.compare(testClass.f, f) !=0)returnfalse;if (Double.compare(testClass.d, d) !=0)returnfalse;return string !=null ? string.equals(testClass.string) : testClass.string ==null;

    }@OverridepublicinthashCode() {int result;long temp;
        result = (int) b;
        result =31 * result + (int) c;
        result =31 * result + (int) s;
        result =31 * result + i;
        result =31 * result + (int) (l ^ (l >>>32));
        result =31 * result + (f != +0.0f ? Float.floatToIntBits(f) :0);
        temp = Double.doubleToLongBits(d);
        result =31 * result + (int) (temp ^ (temp >>>32));
        result =31 * result + (string !=null ? string.hashCode() :0);return result;
    }
}

可以看看JDK内部,对hashCode()的实现,Arrays就是一个很好的例子:

publicstaticinthashCode(boolean a[]) {if (a ==null)return0;int result =1;for (boolean element : a)
            result =31 * result + (element ?1231 :1237);return result;
    }
publicstaticinthashCode(long a[]) {if (a ==null)return0;int result =1;for (long element : a) {int elementHash = (int)(element ^ (element >>>32));
            result =31 * result + elementHash;
        }return result;
    }
//int、short、char、bytepublicstaticinthashCode(int a[]) {if (a ==null)return0;int result =1;for (int element : a)
            result =31 * result + element;return result;
    }

看到这里那你也许会有和我一样的疑惑,为什么是31?

为什么 hashCode 方法选择数字31作为乘子

这个数字居然不是用常量声明的,所以没法从字面意思上推断这个数字的用途。后来带着疑问和好奇心,到网上去找资料查询一下。在看完资料后,默默的感叹了一句,原来是这样啊。那么到底是哪样呢?在接下来章节里,请大家带着好奇心和我揭开数字31的用途之谜。

在详细说明 String hashCode 方法选择数字31的作为乘子的原因之前,我们先来看看 String hashCode 方法是怎样实现的,如下:

publicinthashCode() {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;
}

上面的代码就是 String hashCode 方法的实现,是不是很简单。实际上 hashCode 方法核心的计算逻辑只有三行,也就是代码中的 for 循环。我们可以由上面的 for 循环推导出一个计算公式,hashCode 方法注释中已经给出。如下:

s[0]*31^(n-1) + s[1]*31^(n-2) +... + s[n-1]

这里说明一下,上面的 s 数组即源码中的 val 数组,是 String 内部维护的一个 char 类型数组。这里我来简单推导一下这个公式:

假设 n=3
i=0 -> h =31 *0 + val[0]
i=1 -> h =31 * (31 *0 + val[0]) + val[1]
i=2 -> h =31 * (31 * (31 *0 + val[0]) + val[1]) + val[2]
       h =31*31*31*0 +31*31*val[0] +31*val[1] + val[2]
       h =31^(n-1)*val[0] +31^(n-2)*val[1] + val[2]

上面的公式包括公式的推导并不是本文的重点,大家了解了解即可。接下来来说说本文的重点,即选择31的理由。从网上的资料来看,一般有如下两个原因:

第一,31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。另外一些相近的质数,比如37、41、43等等,也都是不错的选择。那么为啥偏偏选中了31呢?请看第二个原因。

第二、31可以被 JVM 优化,31 * i = (i << 5) - i

上面两个原因中,第一个需要解释一下,第二个比较简单,就不说了。下面我来解释第一个理由。一般在设计哈希算法时,会选择一个特殊的质数。至于为啥选择质数,我想应该是可以降低哈希算法的冲突率。至于原因,这个就要问数学家了,我几乎可以忽略的数学水平解释不了这个原因。上面说到,31是一个不大不小的质数,是优选乘子。那为啥同是质数的2和101(或者更大的质数)就不是优选乘子呢,分析如下。

这里先分析质数2。首先,假设n = 6,然后把质数2n带入上面的计算公式。并仅计算公式中次数最高的那一项,结果是2^5 = 32,是不是很小。所以这里可以断定,当字符串长度不是很长时,用质数2做为乘子算出的哈希值,数值不会很大。也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会导致冲突率上升。

上面说了,质数2做为乘子会导致哈希值分布在一个较小区间内,那么如果用一个较大的大质数101会产生什么样的结果呢?根据上面的分析,我想大家应该可以猜出结果了。就是不用再担心哈希值会分布在一个小的区间内了,因为101^5 = 10,510,100,501。但是要注意的是,这个计算结果太大了。如果用 int 类型表示哈希值,结果会溢出,最终导致数值信息丢失。尽管数值信息丢失并不一定会导致冲突率上升,但是我们暂且先认为质数101(或者更大的质数)也不是很好的选择。最后,我们再来看看质数31的计算结果:31^5 = 28629151,结果值相对于3210,510,100,501来说。是不是很nice,不大不小。

上面用了比较简陋的数学手段证明了数字31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。接下来我会用详细的实验来验证上面的结论,不过在验证前,我们先看看 Stack Overflow 上关于这个问题的讨论,Why does Java’s hashCode() in String use 31 as a multiplier?。其中排名第一的答案引用了《Effective Java》中的一段话,这里也引用一下:

Thevalue31 was chosen becauseit isan odd prime. Ifit were evenandthe multiplication overflowed, information would be lost,as multiplicationby2 is equivalentto shifting. The advantageofusinga prime is lessclear, butit is traditional. A nice propertyof31 is thatthe multiplication can be replacedbya shiftanda subtractionfor better performance:31 * i == (i <<5) - i. Modern VMsdo thissortof optimization automatically.

翻译一下:

选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 *i == (i <<5) -i,现代的 Java 虚拟机可以自动的完成这个优化。

排名第二的答案设这样说的:

As Goodrichand Tamassia point out, If you take over50,000 Englishwords (formedastheunionoftheword lists providedintwo variantsof Unix),usingthe constants31,33,37,39,and41 will produce less than7 collisionsineachcase. Knowing this,it should comeas no surprise that many Java implementations chooseoneof these constants.

翻译一下:

正如 Goodrich 和 Tamassia 指出的那样,如果你对超过50,000个英文单词(由两个不同版本的 Unix 字典合并而成)进行hash code 运算,并使用常数31,33,37,3941 作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面几个常数中,常数31 被 Java 实现所选用也就不足为奇了。

上面的两个答案完美的解释了 Java 源码中选用数字31的原因。第二个答案的验证,感兴趣的朋友可以看看我所参考的原文:https://segmentfault.com/a/1190000010799123

一篇非常干货的文章~

参考链接:

https://segmentfault.com/a/1190000010799123

http://www.jianshu.com/p/039c942b22c0

http://blog.csdn.net/jiangwei0910410003/article/details/22739953

JDK 8 源码

  • 作者:Keozzz
  • 原文链接:https://blog.csdn.net/qq_32971807/article/details/78645922
    更新时间:2022年8月17日11:17:43 ,共 6191 字。