Android性能優化-HashMap

Java集合是我們使用最頻繁的工具之一,但我們對它的理解僅限於使用上,而且大多數情況沒有考慮過其使用規範。最近在使用阿里編程規範插件檢查代碼時會有一些關於HashMap使用方法糾正的提醒。那麼該如何正確使用HashMap呢?

HashMap可能是我們使用最多的鍵值對型的集合類了,它的底層基於哈希表,採用數組存儲數據,使用鏈表來解決哈希碰撞。在JDK1.8中還引入了紅黑樹來解決鏈表長度過長導致的查詢速度下降問題。

HashMap內部是使用一個默認容量爲16的數組來存儲數據的,而數組中每一個元素卻又是一個鏈表的頭結點,所以,更準確的來說,HashMap內部存儲結構是使用哈希表的拉鍊結構

HashMap的結構如下所示:

1696815-787f1bc62bc08c5c.png


性能問題-HashMap大小的問題:

 在android studio中使用阿里代碼規範插件檢查時會有一個關於HashMap初始化大小的檢查,一般HashMap初始化大小都是2的指數冪。爲什麼說HashMap的實際大小總是2的指數冪?因爲就算初始化的時候不是2的指數冪,roundUpToPowerOfTwo函數也會幫你轉。爲什麼一定要是2的指數冪?這個是由於HashMap使用的散列算法,就是用key的hashCode轉成對應的二進制,然後和HashMap的size-1座「&」操作。爲什麼這樣做?舉個例子,如果HashMap的設定大小爲10,那麼roundUpToPowerOfTwo轉完大小是2^4 = 16,那麼16-1=15,用二進制表示就是1111,此時如果一個實例的二進制哈希碼爲850873883(僅用來舉例),二進制表示是110010101101110100111000011011,兩者進行與運算,結果就是截取低四位1011,十進制就是11,也就是進來的key<->value的實體放在11這個位置上。試想如果HashMap的大小不是16而是10,10-1 = 9,二進制表示是1001,那麼中間兩個0的位置永遠不會取到1,也就是2,3,4,6,5,7,這6個位置永遠都都不會被算到實際填充的位置,空間利用率不足一半。這樣的話就明白爲什麼用2的指數冪了:散列均勻。

那麼問題來了,既然HashMap的實現已經幫我們做了這麼多工作,我們是不是直接用就好了,不用管其他的了?明顯不是。首先當HashMap的大小過小的時候,會增加Hash衝突的機率;另外如上面分析put方法說道的第三點,噹噹前的大小達到閥值(默認0.75*size),就會擴容,容量擴大爲原來的兩倍,擴容的過程會遍歷原來的table,把它的元素重新計算在對應的新table中的位置,最壞時間複雜度爲O(n^2);而在hash不衝突的場景下,不需要擴容的話,實際的時間複雜度爲O(1)(只需要按照得到的index放進去)。所以我們最好給HashMap一個初始值,這個值是2的指數冪,並且它呈上裝載因子(默認0.75)後的大小大於我們實際需要的大小。例如,我們實際需要200,那麼200/0.75 約等於267,那麼實際大於方向靠近267的2的指數冪爲2^9 = 512。

性能問題-重寫equals和hashCode方法:

首先明確這兩者的關係: 
A和B對象equals方法返回true,hasCode方法返回值必然一樣; 
A和B對象hashCode不一樣,那麼equals方法必須返回false。 
A和B對象hashCode一樣,不能判定A equals B。 
所以equals方法返回true和hasCode方法返回值一樣是充分非必要的關係。
從Collections.secondaryHash的方法看,最終散列的位置index是和key的hashCode有關的,如果key是引用類型對象,且沒有重寫hashCode,就會很容易出現hash衝突,在put的過程中,發生衝突就會沿着單鏈表遍歷到最後並插入。這個時間複雜度也是O(n)。

同時,put和get的判定都有e.hash == hash && key.equals(e.key),如果不重寫equals方法,默認用「==」判定,比較內存地址。如果key是引用對象,則必須是同一個引用才能判定是相同的對象。例如:

public class TestData {
    public String title;
    TestData(String title){
        this.title = title;
    }
    public static void main(String[] arg){
        HashMap<TestData,String> map = new HashMap<>(8);
        map.put(new TestData("title"),"title");
        String result = map.get(new TestData("title"));
        System.out.println("result="+result);
    }
}
輸出的結果爲null。所以不管是爲了滿足equals和hashCode充分非必要的關係,還是保障程序的健壯性,都應重寫equals。