對幾個通用的Java hashCode重寫方案的一些思考和探討

在我們剛開始學習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[])的具體實現:
?
1
2
3
4
5
6
7
8
9
10
11
12
public static int hashCode(long a[]) {
        if (a == null)
            return 0;
 
        int result = 1;
        for (long element : a) {
            int elementHash = (int)(element ^ (element >>> 32));
            result = 31 * result + elementHash;
        }
 
        return result;
}

Arrays.hashCode(...)只會計算一維數組元素的hashCOde,如果是多維數組,那麼需要遞歸進行hashCode的計算,那麼就需要使用Arrays.deepHashCode(Object[])方法。

3. 最後,要如同上面的代碼,把每個域的散列碼合併到result當中:result = 31 * result + elementHash;
4. 測試,hashCode方法是否符合文章開頭說的基本原則,這些基本原則雖然不能保證性能,但是可以保證不出錯。

這個算法存在這麼幾個問題需要探討:
1. 爲什麼初始值要使用非0的整數?這個的目的主要是爲了減少hash衝突,考慮這麼個場景,如果初始值爲0,並且計算hash值的前幾個域hash值計算都爲0,那麼這幾個域就會被忽略掉,但是初始值不爲0,這些域就不會被忽略掉,示例代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.Serializable;
 
public class Test implements Serializable {
 
    private static final long serialVersionUID = 1L;
 
    private final int[] array;
 
    public Test(int... a) {
        array = a;
    }
 
    @Override
    public int hashCode() {
        int result = 0; //注意,此處初始值爲0
        for (int element : array) {
            result = 31 * result + element;
        }
        return result;
    }
 
    public static void main(String[] args) {
        Test t = new Test(0, 0, 0, 0);
        Test t2 = new Test(0, 0, 0);
        System.out.println(t.hashCode());
        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只需要計算一次就可以了。

?
1
2
3
4
5
6
7
8
9
10
11
12
private volatile int hashCode = 0;
 
@Override
public int hashCode() {
 
    int result = hashCode;
    if(result == 0) {
 
        ...//計算過程
    }
    return result;
}

注意,緩存屬性必須是volatile的,這樣可以在併發訪問環境中保持內存可見性。否則會產生線程安全問題。

此外上面所提到的基本元素類型的hashCode計算,其算法同JDK中其包裝器類型所覆蓋的hashCode邏輯一致
java.lang.Long的hashCode實現:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
     * Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Long}. The result is
     * the exclusive OR of the two halves of the primitive
     * {<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}
     * object. That is, the hashcode is the value of the expression:
     *
     * <blockquote>
     *  {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  (int)(this.longValue()^(this.longValue()>>>32))}
     * </blockquote>
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        return (int)(value ^ (value >>> 32));
    }

java.lang.Float的hashCode實現:

?
1
2
3
4
5
6
7
8
9
10
11
12
/**
  * Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Float} object. The
  * result is the integer bit representation, exactly as produced
  * by the method {<a href="http://my.oschina.net/link1212" target="_blank" rel="nofollow">@link</a>  #floatToIntBits(float)}, of the primitive
  * {<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}
  * object.
  *
  * @return a hash code value for this object.
  */
 public int hashCode() {
     return floatToIntBits(value);
 }

java.lang.double的hashCode實現:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
  * Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Double} object. The
  * result is the exclusive OR of the two halves of the
  * {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  long} integer bit representation, exactly as
  * produced by the method {<a href="http://my.oschina.net/link1212" target="_blank" rel="nofollow">@link</a>  #doubleToLongBits(double)}, of
  * the primitive {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  double} value represented by this
  * {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Double} object. That is, the hash code is the value
  * of the expression:
  *
  * <blockquote>
  *  {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  (int)(v^(v>>>32))}
  * </blockquote>
  *
  * where {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  v} is defined by:
  *
  * <blockquote>
  *  {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  long v = Double.doubleToLongBits(this.doubleValue());}
  * </blockquote>
  *
  * @return  a {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  hash code} value for this object.
  */
 public int hashCode() {
     long bits = doubleToLongBits(value);
     return (int)(bits ^ (bits >>> 32));
 }



org.apache.commons.lang.builder.HashCodeBuilder的實現

org.apache.commons.lang.builder.HashCodeBuilder能夠通過反射機制來自動計算對象的hashCode值。其核心方法是靜態方法:reflectionHashCode,這個方法有多個重載的版本,這些重載版本最終都是調用的 int reflectionHashCode( intinitialNonZeroOddNumber, int multiplierNonZeroOddNumber, Object object,

            boolean testTransients, Class reflectUpToClass, String[] excludeFields)

其餘的版本只是提供不同的默認參數,從而簡化了構建的過程。比如:

?
1
2
3
public static int reflectionHashCode(Object object) {
        return reflectionHashCode(17, 37, object, false, null, null);
}

然後構建的過程是這樣的,除了指定過濾的,比如transient屬性、excludeFields指定的屬性之外,會遍歷其它的類屬性,然後通過反射的方式獲取屬性值,如果屬性是數組,則會遍歷數組,否則會調用屬性的hashCode, 如果是多維數組,會去遞歸取hashCode值,對單個屬性計算hash值的代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public HashCodeBuilder append(Object object) {
        if (object == null) {
            iTotal = iTotal * iConstant;
 
        } else {
            if(object.getClass().isArray()) {
                // 'Switch' on type of array, to dispatch to the correct handler
                // This handles multi dimensional arrays
                if (object instanceof long[]) {
                    append((long[]) object);
                } else if (object instanceof int[]) {
                    append((int[]) object);
                } else if (object instanceof short[]) {
                    append((short[]) object);
                } else if (object instanceof char[]) {
                    append((char[]) object);
                } else if (object instanceof byte[]) {
                    append((byte[]) object);
                } else if (object instanceof double[]) {
                    append((double[]) object);
                } else if (object instanceof float[]) {
                    append((float[]) object);
                } else if (object instanceof boolean[]) {
                    append((boolean[]) object);
                } else {
                    // Not an array of primitives
                    append((Object[]) object);
                }
            } else {
                iTotal = iTotal * iConstant + object.hashCode();
            }
        }
        return this;
    }

這裏要小心,因爲它是直接調用屬性對象的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生成器:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import java.util.Arrays;
 
/**
 * 一個鏈式調用的通用hashCode生成器
 *
 * <a href="http://my.oschina.net/arthor" target="_blank" rel="nofollow">@author</a>  [email protected]
 *
 */
public final class HashCodeHelper {
 
    private static final int multiplierNum = 31;
 
    private int hashCode;
 
    private HashCodeHelper() {
        this(17);
    }
 
    private HashCodeHelper(int hashCode) {
        this.hashCode = hashCode;
    }
 
    public static HashCodeHelper getInstance() {
        return new HashCodeHelper();
    }
 
    public static HashCodeHelper getInstance(int hashCode) {
        return new HashCodeHelper(hashCode);
    }
 
    public HashCodeHelper appendByte(byte val) {
        return appendInt(val);
    }
 
    public HashCodeHelper appendShort(short val) {
        return appendInt(val);
    }
 
    public HashCodeHelper appendChar(char val) {
        return appendInt(val);
    }
 
    public HashCodeHelper appendLong(long val) {
        return appendInt((int) (val ^ (val >>> 32)));
    }
 
    public HashCodeHelper appendFloat(float val) {
        return appendInt(Float.floatToIntBits(val));
    }
 
    public HashCodeHelper appendDouble(double val) {
        return appendLong(Double.doubleToLongBits(val));
    }
 
    public HashCodeHelper appendBoolean(boolean val) {
        return appendInt(val ? 1 : 0);
    }
 
    public HashCodeHelper appendObj(Object... val) {
        return appendInt(Arrays.deepHashCode(val));
    }
 
    public HashCodeHelper appendInt(int val) {
        hashCode = hashCode * multiplierNum + val;
        return this;
    }
 
    public int getHashCode() {
        return this.hashCode;
    }
}

通過這種鏈式調用的方式,沒有反射的開銷,另外可以比較方便的選擇要參與計算的屬性,代碼也比較清晰,但是如果參與計算的屬性值過多,那麼會造成調用鏈過長的情況。爲保持代碼的整潔,也可以分多個鏈來調用。示例代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.Serializable;
 
public class Test implements Serializable{
 
    private static final long serialVersionUID = 1L;
 
    private final int[] array;
 
    public Test(int... a) {
        array = a;
    }
 
    @Override
    public int hashCode() {
        HashCodeHelper hashCodeHelper = HashCodeHelper.getInstance();
        hashCodeHelper.appendInt(array[0]).appendInt(array[1]).appendInt(array[2]);
        hashCodeHelper.appendInt(array[3]).appendInt(array[4]).appendInt(array[5]);
        return hashCodeHelper.getHashCode();
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章