原文鏈接,譯文鏈接,原文作者:?Robert Nystrom,譯者:有孚
本文主要討論下不同的hashCode()的實現對應用程序的性能影響。
hashCode()方法的一個主要作用就是使得對象能夠成為哈希表的key或者散列集的成員。但同時這個對象還得實現equals(Object)方法,它和hashCode()的實現必須是一致的:
- 如果a.equals(b)那么a.hashCode == b.hashCode()
- 如果hashCode()在同一個對象上被調用兩次,它應該返回的是同一個值,這表明這個對象沒有被修改過。
hashCode的性能
從性能的角度來看的話,hashCode()方法的主要目標就是盡量使得不同的對象擁有不同的哈希值。JDK中所有基于哈希的集合都是將值存儲在數組中的。查找元素的時候,會使用哈希值來計算出在數組中的初始查找位置;然后再調用equals()方法將給定的值和數組中存儲對象的值進行比較。如果所有元素的哈希值都不一樣,這會減少哈希的碰撞概率。換句話說,如果所有的值的哈希碼都一樣的話,hashmap(或者hashset)會蛻化成一個列表,操作的時間復雜度會變成O(n2)。
更多細節可以看下hash map碰撞的解決方案。JDK用了一個叫開放尋址的方法,不過還有一種方法叫拉鏈法。所有哈希碼一樣的值都存儲在一個鏈表里(說反了吧)。
我們來看下不同質量的哈希值有什么區別。我們將一個正常的String和它的包裝類進行比較,這個包裝類重寫了hashCode()方法,所有對象都返回同一個哈希值。
| 01 | private static class SlowString |
| 03 | ????public final String m_str; |
| 05 | ????public SlowString( final String str ) { |
| 06 | ????????this.m_str = str; |
| 10 | ????public int hash Code() { |
| 15 | ????public boolean equals(Object o) { |
| 16 | ????????if (this == o) return true; |
| 17 | ????????if (o == null || getClass() != o.getClass()) return false; |
| 18 | ????????final SlowString that = ( SlowString ) o; |
| 19 | ????????return !(m_str != null ? !m_str.equals(that.m_str) : that.m_str != null); |
下面是一個測試方法。后面我們還會再用到它,所以這里還是簡單介紹一下 。它接收一個對象列表,然后對列表中的每個元素依次調用Map.put(), Map.containsKey()方法。
| 01 | private static void testMapSpeed( final List lst, final String name ) |
| 03 | ????final Map<Object, Object> map = new HashMap<Object, Object>( lst.size() ); |
| 05 | ????final long start = System.currentTimeMillis(); |
| 06 | ????for ( final Object obj : lst ) |
| 08 | ????????map.put( obj, obj ); |
| 09 | ????????if ( map.containsKey( obj ) ) |
| 12 | ????final long time = System.currentTimeMillis() - start; |
| 13 | ????System.out.println( "Time for "? + name + " is " + time / 1000.0 + " sec, cnt = " + cnt ); |
String和SlowString對象都是按照”ABCD”+i的格式生成的。處理100000個String對象需要0.041秒,而處理SlowString對象則需要82.5秒。
結果表明,String類的hashCode()方法明顯勝出。我們再做另一個測試。先創建一個字符串列表,前半部分的格式是”ABCdef*&”+i,后半部分的是”ABCdef*&”+i+”ghi”(確保字符串的中間部分變化而結尾不變,不會影響哈希值的質量)。我們會創建1百萬,5百萬,1千萬,2千萬個字符串,來看下有多少字符串是共享哈希值的,同一個哈希值又會被多少個字符串共享。下面是測試的結果:
| 01 | Number of duplicate hash Codes for 1000000 strings = 0 |
| 03 | Number of duplicate hash Codes for 5000000 strings = 196 |
| 04 | Number of hash Code duplicates = 2 count = 196 |
| 06 | Number of duplicate hash Codes for 10000000 strings = 1914 |
| 07 | Number of hash Code duplicates = 2 count = 1914 |
| 09 | Number of duplicate hash Codes for 20000000 strings = 17103 |
| 10 | Number of hash Code duplicates = 2 count = 17103 |
可以看到,共用同一個哈希值的字符串很少,而一個哈希值被兩個以上的字符串共享的概率則非常小。當然了,你的測試數據可能不太一樣——如果用這個測試程序測試你給定的字符串的話。
自動生成long字段的hashCode()方法
許多IDE生成long類型的hashcode()的方式非常值得一提。下面是一個生成的hashCode()方法,這個類有兩個long類型的字段。
| 01 | Number of duplicate hash Codes for 1000000 strings = 0 |
| 03 | Number of duplicate hash Codes for 5000000 strings = 196 |
| 04 | Number of hash Code duplicates = 2 count = 196 |
| 06 | Number of duplicate hash Codes for 10000000 strings = 1914 |
| 07 | Number of hash Code duplicates = 2 count = 1914 |
| 09 | Number of duplicate hash Codes for 20000000 strings = 17103 |
| 10 | Number of hash Code duplicates = 2 count = 17103 |
下面給只有兩個int類型的類生成的方法:
| 1 | public int hash Code() { |
| 3 | result = 31 * result + val2; |
可以看到,long類型的處理是不一樣的。java.util.Arrays.hashCode(long a[])用的也是同樣的方法。事實上,如果你將long類型的高32位和低32位拆開當成int處理的話,生成的hashCode的分布會好很多。下面是兩個long字段的類的改進后的hasCode方法(注意,這個方法運行起來比原來的方法要慢,不過新的hashCode的質量會高很多,這樣的話hash集合的執行效率會得到提高,雖然hashCode本身變慢了)。
| 1 | public int hash Code() { |
| 2 | ????int result = (int) val1; |
| 3 | ????result = 31 * result + (int) (val1 >>> 32); |
| 4 | ????result = 31 * result + (int) val2; |
| 5 | ????return 31 * result + (int) (val2 >>> 32); |
下面是testMapSpeed 方法分別測試10M個這三種對象的結果。它們都是用同樣的值進行初始化的。
| Two longs with original hashCode | Two longs with modified hashCode | Two ints |
| 2.596 sec | 1.435 sec | 0.737 sec |
可以看到,更新后的hashCode方法的效果是不太一樣的。雖然不是很明顯,但是對性能要求很高的地方可以考慮一下它。
高質量的String.hashCode()能做些什么
假設我們有一個map,它是由String標識符來指向某些值。map的key(String標識符)不會在內存的別的地方存儲(某一時間可能有一小部分值是存儲在別的地方)。假設我們已經收集到了map的所有記錄,比如說在某個兩階段算法中的第一個階段。下一步我們要通過key來查找map中的值。我們只會用map里存在的key進行查找。
我們如何能提升map的性能?前面你已經看到了,String.hashCode()返回的幾乎都是不同的值,我們可以掃描所有的key,計算出它們的哈希值,找出那些不唯一的哈希值:
| 01 | Map<Integer, Integer> cnt = new HashMap<Integer, Integer>( max ); |
| 02 | for ( final String s : dict.keySet() ) |
| 04 | ????final int hash = s.hash Code(); |
| 05 | ????final Integer count = cnt.get( hash ); |
| 06 | ????if ( count != null ) |
| 07 | ????????cnt.put( hash, count + 1 ); |
| 09 | ????????cnt.put( hash, 1 ); |
| 12 | //keep only not unique hash codes |
| 13 | final Map<Integer, Integer> mult = new HashMap<Integer, Integer>( 100 ); |
| 14 | for ( final Map.Entry<Integer, Integer> entry : cnt.entrySet() ) |
| 16 | ????if ( entry.getValue() > 1 ) |
| 17 | ????????mult.put( entry.getKey(), entry.getValue() ); |
現在我們可以創建兩個新的map。為了簡單點,假設map里存的值就是Object。在這里,我們創建了Map<Integer, Object> 和Map<String, Object>(生產環境推薦使用TIntObjectHashMap)兩個map。第一個map存的是那些唯一的hashcode以及對應的值,而第二個,存的是那些hashCode不唯一的字符串以及它們相應的值。
| 01 | final Map<Integer, Object> unique = new HashMap<Integer, Object>( 1000 ); |
| 02 | final Map<String, Object> not_unique = new HashMap<String, Object>( 1000 ); |
| 05 | for ( final Map.Entry<String, Object> entry : dict.entrySet() ) |
| 07 | ????final int hash Code = entry.getKey().hash Code(); |
| 08 | ????if ( mult.containsKey( hash Code ) ) |
| 09 | ????????not_unique.put( entry.getKey(), entry.getValue() ); |
| 11 | ????????unique.put( hash Code, entry.getValue() ); |
| 14 | //keep only not unique hash codes |
| 15 | final Map<Integer, Integer> mult = new HashMap<Integer, Integer>( 100 ); |
| 16 | for ( final Map.Entry<Integer, Integer> entry : cnt.entrySet() ) |
| 18 | ????if ( entry.getValue() > 1 ) |
| 19 | ????????mult.put( entry.getKey(), entry.getValue() ); |
現在,為了查找某個值,我們得先查找第一個hashcode唯一的map,如果沒找到,再查找第二個不唯一的map:
| 1 | public Object get( final String key ) |
| 3 | final int hash Code = key.hash Code(); |
| 4 | Object value = m_unique.get( hash Code ); |
| 6 | value = m_not_unique.get( key ); |
在一些不太常見的情況下,你的這個不唯一的map里的對象可能會很多。碰到這種情況的話,先嘗試用java.util.zip.CRC32或者是java.util.zip.Adler32來替換掉hashCode()的實現(Adler32比CRC32要快,不過它的分布較差些)。如果實在不行,再嘗試用兩個不同的函數來計算哈希值:低32位和高32位分別用不同的函數生成。hash函數就用Object.hashCode, java.util.zip.CRC32或者java.util.zip.Adler32。
(譯注:這么做的好處就是壓縮了map的存儲空間,比如你有一個map,它的KEY存100萬個字符串的話,壓縮了之后就只剩下long類型以及很少的字符串了)
set的壓縮效果更明顯
前面那個例子中,我們討論了如何去除map中的key值。事實上,優化set的話效果會更加明顯。set大概會有這么兩個使用場景:一個是將原始的set拆分成多個子set,然后依次查詢標識符是否屬于某個子set;還有就是是作為一個拼寫檢查器(spellchecker )——有些要查詢的值是預想不到的值(比如拼寫錯誤了),而就算出了些錯誤的話影響也不是很大(如果碰巧另一個單詞也有同樣的hashCode,你會認為這個單詞是拼寫正確的)。這兩種場景set都非常適用。
如果我們延用前面的方法的話,我們會得到一個唯一的hashcode組成的Set,以及不唯一的hashCode組成的一個Set。這里至少能優化掉不少字符串存儲的空間。
如果我們可以把哈希值的取值限制在一定的區間內(比如說2^20),那么我們可以用一個BitSet來代替Set,這個在BitSet一文中已經提到了。一般來說如果我們提前知道原始set的大小的話,哈希值的范圍是有足夠的優化空間的。
下一步就是確定有多少標識符是共享相同的哈希值的。如果碰撞的哈希值比較多的話,改進下你的hashCode()方法,或者擴大哈希值的取值范圍。最完美的情況就是你的標記符全都有唯一的hashcode( 這其實不難實現)。優化完的好處就是,你只需要一個BitSet就夠了,而不需要存儲一個大的字符串集合。
總結
改進你的hashCode算法的分布。優化它比優化這個方法的執行速度要重要多了。千萬不要寫一個返回常量的hashCode方法。
String.hashCode的實現已經相當完美了,因此很多時候你可以用String的hashCode來代替字符串本身了。如果你使用的是字符串的set,試著把它優化成BitSet。這將大大提升你程序的性能。
本文最早發表于Java譯站
總結
以上是生活随笔為你收集整理的hashCode()方法的性能优化的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。