天天看点

Java 重写Object类中equals和hashCode方法

一:怎样重写equals()方法? 

  重写equals()方法看起来非常简单,但是有许多改写的方式会导致错误,并且后果非常严重。要想正确改写equals()方法,你必须要遵守它的通用约定。下面是约定的内容,来自java.lang.Object的规范: 

equals方法实现了等价关系(equivalence relation): 

1. 自反性:对于任意的引用值x,x.equals(x)一定为true。 

2. 对称性:对于任意的引用值x 和 y,当x.equals(y)返回true时, 

  y.equals(x)也一定返回true。 

3. 传递性:对于任意的引用值x、y和z,如果x.equals(y)返回true, 

  并且y.equals(z)也返回true,那么x.equals(z)也一定返回true。 

4. 一致性:对于任意的引用值x 和 y,如果用于equals比较的对象信息没有被修 

  改,多次调用x.equals(y)要么一致地返回true,要么一致地返回false。 

5. 非空性:对于任意的非空引用值x,x.equals(null)一定返回false。 

二:重写equals方法的要点: 

1. 使用==操作符检查“实参是否为指向对象的一个引用”。 

2. 使用instanceof操作符检查“实参是否为正确的类型”。 

3. 把实参转换到正确的类型。 

4. 对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹 

  配。对于既不是float也不是double类型的基本类型的域,可以使用==操作符 

  进行比较;对于对象引用类型的域,可以递归地调用所引用的对象的equals方法; 

  对于float类型的域,先使用Float.floatToIntBits转换成int类型的值, 

  然后使用==操作符比较int类型的值;对于double类型的域,先使用 

  Double.doubleToLongBits转换成long类型的值,然后使用==操作符比较 

  long类型的值。 

5. 当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传 

  递的、一致的?(其他两个特性通常会自行满足)如果答案是否定的,那么请找到 

  这些特性未能满足的原因,再修改equals方法的代码。

三:hashCode

hashCode主要是用于散列集合,通过对象hashCode返回值来与散列中的对象进行匹配,通过hashCode来查找散列中对象的效率为O(1),如果多个对象具有相同的hashCode,那么散列数据结构在同一个hashCode位置处的元素为一个链表,需要通过遍历链表中的对象,并调用equals来查找元素。这也是为什么要求如果对象通过equals比较返回true,那么其hashCode也必定一致的原因。

为对象提供一个高效的hashCode算法是一个很困难的事情。理想的hashCode算法除了达到本文最开始提到的要求之外,还应该是为不同的对象产生不相同的hashCode值,这样在操作散列的时候就完全可以达到O(1)的查找效率,而不必去遍历链表。假设散列中的所有元素的hashCode值都相同,那么在散列中查找一个元素的效率就变成了O(N),这同链表没有了任何的区别。

hashCode()的返回值和equals()的关系如下:

  • 如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。
  • 如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。

四. 设计 hashCode() [1] 把某个非零常数值,例如 17 ,保存在 int 变量 result 中; [2] 对于对象中每一个关键域 f (指 equals 方法中考虑的每一个域): [2.1]boolean 型,计算 (f ? 0 : 1); [2.2]byte,char,short 型,计算 (int); [2.3]long 型,计算 (int) (f ^ (f>>>32)); [2.4]float 型,计算 Float.floatToIntBits( afloat ) ; [2.5]double 型,计算 Double.doubleToLongBits( adouble ) 得到一个 long ,再执行 [2.3]; [2.6] 对象引用,递归调用它的 hashCode 方法 ; [2.7] 数组域,对其中每个元素调用它的 hashCode 方法。 [3] 将上面计算得到的散列码保存到 int 变量 c ,然后执行  result=31*result+c; [4] 返回 result 。   这个算法存在这么几个问题需要探讨: 1. 为什么初始值要使用非0的整数?这个的目的主要是为了减少hash冲突,考虑这么个场景,如果初始值为0,并且计算hash值的前几个域hash值计算都为0,那么这几个域就会被忽略掉,但是初始值不为0,这些域就不会被忽略掉,示例代码:  

01

import

java.io.Serializable;

02

03

public

class

Test 

implements

Serializable {

04

05

private

static

final

long

serialVersionUID = 1L;

06

07

private

final

int

[] array;

08

09

public

Test(

int

... a) {

10

array = a;

11

}

12

13

@Override

14

public

int

hashCode() {

15

int

result = 

//注意,此处初始值为0

16

for

(

int

element : array) {

17

result = 

31

* result + element;

18

}

19

return

result;

20

}

21

22

public

static

void

main(String[] args) {

23

Test t = 

new

Test(

);

24

Test t2 = 

new

Test(

);

25

System.out.println(t.hashCode());

26

System.out.println(t2.hashCode());

27

}

28

29

}

如果hashCode中result的初始值为0,那么对象t和对象t2的hashCode值都会为0,尽管这两个对象不同。但如果result的值为17,那么计算hashCode的时候就不会忽略这些为0的值,最后的结果t1是15699857,t2是506447   2. 为什么每次需要使用乘法去操作result? 主要是为了使散列值依赖于域的顺序,还是上面的那个例子,Test t = new Test(1, 0)跟Test t2 = new Test(0, 1), t和t2的最终hashCode返回值是不一样的。   3. 为什么是31? 31是个神奇的数字,因为任何数n * 31就可以被JVM优化为 (n << 5) -n,移位和减法的操作效率要比乘法的操作效率高的多。