哈希码
问: hashCode()
方法是用来做什么的?
答: hashCode()
方法返回给调用者此对象的哈希码(其值由一个hash函数计算得来)。这个方法通常用在基于hash的集合类中,像java.util.HashMap
,java.until.HashSet
和java.util.Hashtable
.
问: 在类中覆盖equals()
的时候,为什么要同时覆盖hashCode()
?
答: 在覆盖equals()
的时候同时覆盖hashCode()
可以保证对象的功能兼容于hash集合。这是一个好习惯,即使这些对象不会被存储在hash集合中。
问: hashCode()
有什么一般规则?
答: hashCode()
的一般规则如下:
- 在同一个Java程序中,对一个相同的对象,无论调用多少次
hashCode()
,hashCode()
返回的整数必须相同,因此必须保证equals()
方法比较的内容不会更改。但不必在另一个相同的Java程序中也保证返回值相同。 - 如果两个对象用
equals()
方法比较的结果是相同的,那么这两个对象调用hashCode()
应该返回相同的整数值。 - 当两个对象使用
equals()
方法比较的结果是不同的,hashCode()
返回的整数值可以不同。然而,hashCode()
的返回值不同可以提高哈希表的性能。
问: 如果覆盖了equals()
却不覆盖hashCode()
会有什么后果?
答: 当覆盖equals()
却不覆盖hashCode()
的时候,在hash集合中存储对象时就会出现问题。例如,参考代码清单2.
代码清单2:当hash集合只覆盖equals()
时的问题
1 import java.util.HashMap; 2 import java.util.Map; 3 4 final class Employee 5 { 6 private String name; 7 private int age; 8 9 Employee(String name, int age) 10 { 11 this.name = name; 12 this.age = age; 13 } 14 15 @Override 16 public boolean equals(Object o) 17 { 18 if (!(o instanceof Employee)) 19 return false; 20 21 Employee e = (Employee) o; 22 return e.getName().equals(name) && e.getAge() == age; 23 } 24 25 String getName() 26 { 27 return name; 28 } 29 30 int getAge() 31 { 32 return age; 33 } 34 } 35 36 public class HashDemo 37 { 38 public static void main(String[] args) 39 { 40 Map<Employee, String> map = new HashMap<>(); 41 Employee emp = new Employee("John Doe", 29); 42 map.put(emp, "first employee"); 43 System.out.println(map.get(emp)); 44 System.out.println(map.get(new Employee("John Doe", 29))); 45 } 46 }
代码清单2声明了一个Employee
类,覆盖了equals()
方法但是没有覆盖hashCode()
。同时声明了一个一个HashDemo
类,来演示将Employee
作为键存储时时产生的问题。main()
函数首先在实例化Employee
之后创建了一个hashmap,将Employee
对象作为键,将一个字符串作为值来存储。然后它将这个对象作为键来检索这个集合并输出结果。同样地,再通过新建一个具有相同内容的Employee
对象作为键来检索集合,输出信息。
编译(javac HashDemo.java
)并运行(java HashDemo
)代码清单2,你将看到如下输出结果:
1
2
|
first employee null |
如果hashCode()
方法被正确的覆盖,你将在第二行看到first employee
而不是null
,因为这两个对象根据equals()
方法比较的结果是相同的,根据上文中提到的规则2:如果两个对象用equals()
方法比较的结果是相同的,那么这两个对象调用hashCode()
应该返回相同的整数值。
问: 如何正确的覆盖hashCode()
?
答: Joshua Bloch的《Effective Java》第八版中给出了一个四步法来正确的覆盖hashCode()
。下面的步骤和Bloch的方法类似。
- 声明一个
int
型的变量,命名为result
(或者其他你喜欢的名字),然后初始化为一个不为零的常量(比如31)。使用一个不为零的常量会影响到所有的初始的哈希值(步骤2.1的结果)为零的值。【A nonzero value is used so that it will be affected by any initial fields whose hash value (computed in Step 2.1) is zero. 】如果初始的result
为0
的话,最后的哈希值不会被它影响到,所以冲突的几率会增加。这个非零result
值是任意的。 - 对每一个对象中有意义的具体值(在
equals()
中所涉及的值),f
,进行以下步骤的处理:- 按照以下步骤计算f的基于
int
型的哈希值hc
:- 对于一个
boolean
型变量,hc = f? 0 : 1;
。 - 对于一个
byte
,char
,short
,或者int
型变量,hc = (int)f;
. - 对于一个
long
型变量,hc = (int) (f ^ (f >>> 32));
.这个表达式是将long
型变量作为32位(long
型最多有32位)来计算的; - 对于一个
float
型变量,hc = Float.floatToIntBits(f);
. - 对于一个
double
型变量,long l = Double.doubleToLongBits(f); hc = (int) (l ^ (l >>> 32));
. - 对于引用类型的变量,如果类中的
equals()
方法递归的调用equals()
类比较成员变量,那么就递归调用hashCode()
;如果需要更复杂的比较,就计算这个值的“标准表示”来脚酸标准的哈希值;如果引用类型的值为null
,f = 0
. - 对于一个数组类型的引用,将每一个元素视为单独的变量,对于每一个有意义的值,调用对应的方法计算其哈希值,最后如步骤2.2的描述那样将所有的哈希值合并。
- 对于一个
- 计算
result = 37*result+hc
,将所有的hc
合并到哈希值中。乘法使哈希值取决于它的值的规则,当一个类中存在多种相似的值时,就增加了哈希表的离散性。 - 返回result。
- 完成
hashCode()
之后,要确保相同的对象调用hashCode()
得到相同的哈希值。
- 按照以下步骤计算f的基于
举例说明上面这个方法,代码清单3是代码清单2的第二个版本,它的Employee
类重写了hashCode()
。
代码清单3:正确地覆盖hashCode()
1 import java.util.HashMap; 2 import java.util.Map; 3 4 final class Employee 5 { 6 private String name; 7 private int age; 8 9 Employee(String name, int age) 10 { 11 this.name = name; 12 this.age = age; 13 } 14 15 @Override 16 public boolean equals(Object o) 17 { 18 if (!(o instanceof Employee)) 19 return false; 20 21 Employee e = (Employee) o; 22 return e.getName().equals(name) && e.getAge() == age; 23 } 24 25 String getName() 26 { 27 return name; 28 } 29 30 int getAge() 31 { 32 return age; 33 } 34 35 @Override 36 public int hashCode() 37 { 38 int result = 31; 39 result = 37*result+name.hashCode(); 40 result = 37*result+age; 41 return result; 42 } 43 } 44 45 public class HashDemo 46 { 47 public static void main(String[] args) 48 { 49 Map<Employee, String> map = new HashMap<>(); 50 Employee emp = new Employee("John Doe", 29); 51 map.put(emp, "first employee"); 52 System.out.println(map.get(emp)); 53 System.out.println(map.get(new Employee("John Doe", 29))); 54 } 55 }
代码清单3的Employee
类中声明了两个在hashCode()
都涉及到的值。覆盖的hashCode()
方法首先初始化result
为31
,然后将String
类型的name
变量和int
型的age
变量的哈希值合并到result
中,随后返回result
。
编译(javac HashDemo.java
)并运行(java HashDemo
)代码清单3,你将看到如下输出结果:
1
2
|
first employee first employee |