每个类方法中都存在equals和hashCode方法,因为每个类都继承了Object方法。
Object中equlas和hashCode方法如下:
1 | public boolean equals(Object obj) { |
1 | public native int hashCode(); |
equals和hashCode都没有被final修饰那么它们是可以被重写的,其中hashCode被native修饰那么它是调用其他语言的方法。
可以看到equals方法默认比较的是对象的引用,直接用“==”进行比较。而hashCode方法是一个native方法,返回值为整型。
1.equals方法的相关约定规范:
在Object类的equals方法上有注释说明了equals()方法需满足的一些特性:
- 自反性(reflexive)。对于任意不为null的引用值x,x.equals(x)一定是true;
- 对称性(symmetric)。对于任意不为null的引用值x和y,当且仅当x.equals(y)是true时,y.equals(x)也是true;
- 传递性(transitive)。对于任意不为null的引用值x、y和z,如果x.equals(y)是true,同时y.equals(z)是true,那么x.equals(z)一定是true;
- 一致性(consistent)。对于任意不为null的引用值x和y,如果用于equals比较的对象信息没有被修改的话,多次调用时x.equals(y)要么一致地返回true要么一致地返回false;
- 对于任意不为null的引用值x,x.equals(null)返回false;
对照上面特质,我们发现Object方法直接比较的是两个引用地址,只有两个地址相同才相等,也就是说是差别可能性最大的等价关系
hashCode()方法返回对象的一个hash code值。该方法被用于hash tables,如HashSet、HashMap。
Object中的native hashCode()方法将对象在内存中的地址作为哈希码返回,可以保证不同对象的返回值不同。
2.String中的equals方法
1 | public boolean equals(Object anObject) { |
String的equals方法,不仅包含应用地址相同这种情况,还包括里面所存储的字符串值相同的情况。也就是说虽然是两个String对象,但是它们的字符串值相等,那么equals方法返回的结果就是true。这也正是大多数情况下我们所说的“equals方法比较的是值”。
由于Object的equals方法的默认特例存在,因此在没有自定义equals方法时,我们不能一概的说equals方法比较的是具体的值,而“==”比较的是引用。
3.Stirng中的hashCode方法
1 | public int hashCode() { |
上述hash值的计算注释中有说明,基本公式为:s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]。
其中, s[i]是字符串的第i个字符,n是字符串的长度,^表示求幂(空字符串的哈希码为0)。
计算过程中使用数字31,主要有以下原因:
1、由于质数的特性,它与其他数字相乘之后,计算结果唯一的概率更大,哈希冲突的概率更小。
2、使用的质数越大,哈希冲突的概率越小,但是计算的速度也越慢;31是哈希冲突和性能的折中,实际上是实验观测的结果。
3、JVM会自动对31进行优化:31 * i == (i << 5) - i
hashCode()方法的作用
hashCode()方法主要用于hash表中,比如HashSet、HashMap等。
我们先来看一下ArrayList,它的底层是数组,每个数据往底层的数组中存取即可,数据不需要判断是否重复。
集合Set中的元素是无序不可重复的,那么如何确保存入的元素不重复呢?逐个调用equals()方法进行比较?数据量少的时候还可以,但数据量大了时间复杂度基本上是O(n),会出现性能问题。
Java中采用哈希算法来解决这个问题,将对象(或数据)依特定算法直接映射到一个地址上,这样时间复杂度趋于O(1),对象的存取效率大大提高。
集合Set添加某元素时,先调用hashCode()方法,定位到此元素实际存储位置,如果这个位置没有元素,说明是第一次存储;若此位置有对象存在,调用equals()进行比较,相等就舍弃此元素不存,不等则散列到其他地址。
上面的示例也说明了为什么equals()相等,则hashCode()必须相等,进而当重写了equals方法,也要对hashCode()方法进行重写。
HashMap的基本处理机制与HashSet很类似,只不过底层的数据存储结构有所不同而已。
简而言之,在集合查找时,hashcode能极大的降低对象比较次数,提高查找效率。
hashCode()方法的性质
hashCode的实现也有一定的要求,相关英文说明在Object的equals方法注解上:
- 在一个Java应用的执行期间,如果一个对象提供给equals做比较的信息没有被修改的话,该对象多次调用hashCode()方法,该方法必须始终如一返回同一个integer。
- 如果两个对象根据equals(Object)方法是相等的,那么调用二者各自的hashCode()方法必须产生同一个integer结果。
并不要求根据equals(java.lang.Object)方法不相等的两个对象,调用二者各自的hashCode()方法必须产生不同的integer结果。但对于不同的对象产生不同的integer结果,有可能会提高hash table的性能。
如何重写hashCode()
《Effective Java》中提供了一种简单通用的hashCode算法。
A、初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;
B、选取equals方法中用于比较的所有域(之所以只选择equals()中使用的域,是为了保证上述原则的第1条),然后针对每个域的属性进行计算:
- (1) 如果是boolean值,则计算f ? 1:0
- (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) 如果是数组,那么需要为每个元素当做单独的域来处理。java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上。
C、最后,把每个域的散列码合并到对象的哈希码中。
小结
关于equals方法很明确的是用于比较两个对象是否相等。而对于hashCode方法重点是为了在类似HashMap场景下提升效率,只算是技术要求。
在集合中通常通过equals方法来比较对象是否相等,通过hashCode方法来解决大数据量时会发生的性能问题。
在实践中我们很少使用Object对象来作为Map的key,也是因为如果Object对象的属性变了,会导致hashCode变化,进而可能会导致找不到对应值,而String是不可变的对象,作为key就很适合。