http://my.oschina.net/chihz/blog/56256
在我们刚开始学习Java的时候就被教导,在编写类的时候,如果覆盖了Object的equals方法,那么必须要覆盖hashCode方法,并且如果两个对象用equals方法比较返回true,那么这两个对象hashCode返回的值也必须是相等的,并且对于同一个对象,equals方法需要比较的属性值没有被修改,那么每次调用hashCode返回的值应该是一致的。
hashCode主要是用于散列集合,通过对象hashCode返回值来与散列中的对象进行匹配,通过hashCode来查找散列中对象的效率为O(1),如果多个对象具有相同的hashCode,那么散列数据结构在同一个hashCode位置处的元素为一个链表,需要通过遍历链表中的对象,并调用equals来查找元素。这也是为什么要求如果对象通过equals比较返回true,那么其hashCode也必定一致的原因。
为对象提供一个高效的hashCode算法是一个很困难的事情。理想的hashCode算法除了达到本文最开始提到的要求之外,还应该是为不同的对象产生不相同的hashCode值,这样在操作散列的时候就完全可以达到O(1)的查找效率,而不必去遍历链表。假设散列中的所有元素的hashCode值都相同,那么在散列中查找一个元素的效率就变成了O(N),这同链表没有了任何的区别。
这种理想的hashCode算法,如果是为具体业务的对象去设计应该不是很难,比如很多的数据库映射对象都存在一个整形的id属性,这个id属性往往在整个系统中是唯一的,那么hashCode在重写的时候返回这个id的值就可以了,equals比较的时候也是去比较id的值,并且对象在从数据库初始化之后是不可变的,这样就完全达到了理想的情况。这些对象保存在散列中,查找效率会是完全的O(1),不需要遍历任何链表。
本文着重讨论的是通用的hashCode实现,所谓的通用就是适合Java中每一个对象的hashCode算法实现。每个类的结构不尽相同,要想产生一个适合所有场景的理想hashCode算法几乎是不可能的,要设计通用的hashCode算法,我们只能去不断接近理想的情况。下面是几种实现方式。
《Effective Java》中推荐的实现方式
Google首席Java架构师Joshua Bloch在他的著作《Effective Java》中提出了一种简单通用的hashCode算法
1. 初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;
2. 选取equals方法中用于比较的所有域,然后针对每个域的属性进行计算:
(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) 如果是数组,那么需要为每个元素当做单独的域来处理。如果你使用的是1.5及以上版本的JDK,那么没必要自己去 重新遍历一遍数组,java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上,
java.util.Arrays.hashCode(long[])的具体实现:
01 |
public static int hashCode( long a[]) { |
06 |
for ( long element : a) { |
07 |
int elementHash = ( int )(element ^ (element >>> 32 )); |
08 |
result = 31 * result + elementHash; |
Arrays.hashCode(...)只会计算一维数组元素的hashCOde,如果是多维数组,那么需要递归进行hashCode的计算,那么就需要使用Arrays.deepHashCode(Object[])方法。
3. 最后,要如同上面的代码,把每个域的散列码合并到result当中:result = 31 * result + elementHash;
4. 测试,hashCode方法是否符合文章开头说的基本原则,这些基本原则虽然不能保证性能,但是可以保证不出错。
这个算法存在这么几个问题需要探讨:
1. 为什么初始值要使用非0的整数?这个的目的主要是为了减少hash冲突,考虑这么个场景,如果初始值为0,并且计算hash值的前几个域hash值计算都为0,那么这几个域就会被忽略掉,但是初始值不为0,这些域就不会被忽略掉,示例代码:
01 |
import java.io.Serializable; |
03 |
public class Test implements Serializable { |
05 |
private static final long serialVersionUID = 1L; |
07 |
private final int [] array; |
09 |
public Test( int ... a) { |
14 |
public int hashCode() { |
16 |
for ( int element : array) { |
17 |
result = 31 * result + element; |
22 |
public static void main(String[] args) { |
23 |
Test t = new Test( 0 , 0 , 0 , 0 ); |
24 |
Test t2 = new Test( 0 , 0 , 0 ); |
25 |
System.out.println(t.hashCode()); |
26 |
System.out.println(t2.hashCode()); |
如果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,移位和减法的操作效率要比乘法的操作效率高的多。
另外如果对象是不可变的,那么还推荐使用缓存的方式,在对象中使用一个单独的属性来存储hashCode的值,这样对于这个对象来说hashCode只需要计算一次就可以了。
01 |
private volatile int hashCode = 0 ; |
04 |
public int hashCode() { |
06 |
int result = hashCode; |
注意,缓存属性必须是volatile的,这样可以在并发访问环境中保持内存可见性。否则会产生线程安全问题。
此外上面所提到的基本元素类型的hashCode计算,其算法同JDK中其包装器类型所覆盖的hashCode逻辑一致
java.lang.Long的hashCode实现:
02 |
* Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> Long}. The result is |
03 |
* the exclusive OR of the two halves of the primitive |
04 |
* {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> long} value held by this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> Long} |
05 |
* object. That is, the hashcode is the value of the expression: |
08 |
* {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> (int)(this.longValue()^(this.longValue()>>>32))} |
11 |
* @return a hash code value for this object. |
13 |
public int hashCode() { |
14 |
return ( int )(value ^ (value >>> 32 )); |
java.lang.Float的hashCode实现:
02 |
* Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> Float} object. The |
03 |
* result is the integer bit representation, exactly as produced |
04 |
* by the method {<a href="http://my.oschina.net/link1212" target="_blank" rel="nofollow">@link</a> #floatToIntBits(float)}, of the primitive |
05 |
* {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> float} value represented by this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> Float} |
08 |
* @return a hash code value for this object. |
10 |
public int hashCode() { |
11 |
return floatToIntBits(value); |
java.lang.double的hashCode实现:
02 |
* Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> Double} object. The |
03 |
* result is the exclusive OR of the two halves of the |
04 |
* {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> long} integer bit representation, exactly as |
05 |
* produced by the method {<a href="http://my.oschina.net/link1212" target="_blank" rel="nofollow">@link</a> #doubleToLongBits(double)}, of |
06 |
* the primitive {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> double} value represented by this |
07 |
* {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> Double} object. That is, the hash code is the value |
11 |
* {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> (int)(v^(v>>>32))} |
14 |
* where {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> v} is defined by: |
17 |
* {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> long v = Double.doubleToLongBits(this.doubleValue());} |
20 |
* @return a {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a> hash code} value for this object. |
22 |
public int hashCode() { |
23 |
long bits = doubleToLongBits(value); |
24 |
return ( int )(bits ^ (bits >>> 32 )); |
org.apache.commons.lang.builder.HashCodeBuilder的实现
org.apache.commons.lang.builder.HashCodeBuilder能够通过反射机制来自动计算对象的hashCode值。其核心方法是静态方法:reflectionHashCode,这个方法有多个重载的版本,这些重载版本最终都是调用的 intreflectionHashCode( int initialNonZeroOddNumber, int multiplierNonZeroOddNumber, Object object,
boolean testTransients, Class reflectUpToClass, String[] excludeFields)
其余的版本只是提供不同的默认参数,从而简化了构建的过程。比如:
1 |
public static int reflectionHashCode(Object object) { |
2 |
return reflectionHashCode( 17 , 37 , object, false , null , null ); |
然后构建的过程是这样的,除了指定过滤的,比如transient属性、excludeFields指定的属性之外,会遍历其它的类属性,然后通过反射的方式获取属性值,如果属性是数组,则会遍历数组,否则会调用属性的hashCode, 如果是多维数组,会去递归取hashCode值,对单个属性计算hash值的代码如下:
01 |
public HashCodeBuilder append(Object object) { |
03 |
iTotal = iTotal * iConstant; |
06 |
if (object.getClass().isArray()) { |
09 |
if (object instanceof long []) { |
10 |
append(( long []) object); |
11 |
} else if (object instanceof int []) { |
12 |
append(( int []) object); |
13 |
} else if (object instanceof short []) { |
14 |
append(( short []) object); |
15 |
} else if (object instanceof char []) { |
16 |
append(( char []) object); |
17 |
} else if (object instanceof byte []) { |
18 |
append(( byte []) object); |
19 |
} else if (object instanceof double []) { |
20 |
append(( double []) object); |
21 |
} else if (object instanceof float []) { |
22 |
append(( float []) object); |
23 |
} else if (object instanceof boolean []) { |
24 |
append(( boolean []) object); |
27 |
append((Object[]) object); |
30 |
iTotal = iTotal * iConstant + object.hashCode(); |
这里要小心,因为它是直接调用属性对象的hashCode,如果是基本类型,那么就会调用包装器中提供的hashCode方法,如果是引用类型,那么需要仔细检查引用类型的hashCode方法,以免产生违反hashCode基本原则的情况。
然后剩下的计算过程,同Effective Java中描述的基本类似,不过这里的hash初值和乘数因子可以自己来设置,默认的情况是使用了17 和 37两个互质数。
HashCodeBuilder最大好处是使用方便,一行代码就能搞定hashCode的重写问题,并且让代码很清晰,但是它有这么几个值得注意的地方:
1. 使用反射会对程序的性能造成影响,不过Java反射机制为了把性能影响降到最低,对类似getFields()之类的操作都采用了Cache策略,对一般的程序而言,这些性能开销往往可以忽略不计。另外如果使用的是不可变对象,那么强烈建议把hashCode Cache住,这样可以极大的提高hashCode计算的性能。
2. 因为默认会处理所有的field(除了transient修饰的field),所以一定要测试是否违反hashCode的基本原则(为了保障基本原则的正确,建议跟org.apache.commons.lang.EqualsBuilder搭配使用),尤其是当类的域中包含引用类型的时候,一定要递归检查引用类型的hashCode.
链式的HashCodeBuilder
最后结合Effective Java中提供的算法编写一个链式的HashCode生成器:
01 |
import java.util.Arrays; |
04 |
* 一个链式调用的通用hashCode生成器 |
06 |
* <a href="http://my.oschina.net/arthor" target="_blank" rel="nofollow">@author</a> hongze.chi@gmail.com |
09 |
public final class HashCodeHelper { |
11 |
private static final int multiplierNum = 31 ; |
15 |
private HashCodeHelper() { |
19 |
private HashCodeHelper( int hashCode) { |
20 |
this .hashCode = hashCode; |
23 |
public static HashCodeHelper getInstance() { |
24 |
return new HashCodeHelper(); |
27 |
public static HashCodeHelper getInstance( int hashCode) { |
28 |
return new HashCodeHelper(hashCode); |
31 |
public HashCodeHelper appendByte( byte val) { |
32 |
return appendInt(val); |
35 |
public HashCodeHelper appendShort( short val) { |
36 |
return appendInt(val); |
39 |
public HashCodeHelper appendChar( char val) { |
40 |
return appendInt(val); |
43 |
public HashCodeHelper appendLong( long val) { |
44 |
return appendInt(( int ) (val ^ (val >>> 32 ))); |
47 |
public HashCodeHelper appendFloat( float val) { |
48 |
return appendInt(Float.floatToIntBits(val)); |
51 |
public HashCodeHelper appendDouble( double val) { |
52 |
return appendLong(Double.doubleToLongBits(val)); |
55 |
public HashCodeHelper appendBoolean( boolean val) { |
56 |
return appendInt(val ? 1 : 0 ); |
59 |
public HashCodeHelper appendObj(Object... val) { |
60 |
return appendInt(Arrays.deepHashCode(val)); |
63 |
public HashCodeHelper appendInt( int val) { |
64 |
hashCode = hashCode * multiplierNum + val; |
68 |
public int getHashCode() { |
通过这种链式调用的方式,没有反射的开销,另外可以比较方便的选择要参与计算的属性,代码也比较清晰,但是如果参与计算的属性值过多,那么会造成调用链过长的情况。为保持代码的整洁,也可以分多个链来调用。示例代码:
01 |
import java.io.Serializable; |
03 |
public class Test implements Serializable{ |
05 |
private static final long serialVersionUID = 1L; |
07 |
private final int [] array; |
09 |
public Test( int ... a) { |
14 |
public int hashCode() { |
15 |
HashCodeHelper hashCodeHelper = HashCodeHelper.getInstance(); |
16 |
hashCodeHelper.appendInt(array[ 0 ]).appendInt(array[ 1 ]).appendInt(array[ 2 ]); |
17 |
hashCodeHelper.appendInt(array[ 3 ]).appendInt(array[ 4 ]).appendInt(array[ 5 ]); |
18 |
return hashCodeHelper.getHashCode(); |