2019-05-15 HashMap優化的幾種簡單方法

畫重點,面試加分

先貼出HashMap源碼普及一下幾個概念:

public class HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable{    //  默認的初始容量(容量為HashMap中桶的數目)是16,且實際容量必須是2的整數次冪。     static final int DEFAULT_INITIAL_CAPACITY = 16;     // 最大容量(必須是2的冪且小于2的30次方,傳入容量過大將被這個值替換)    static final int MAXIMUM_CAPACITY = 1 << 30;     // 默認加載因子    static final float DEFAULT_LOAD_FACTOR = 0.75f;     // 存儲數據的Entry數組,長度是2的冪。    // HashMap是采用拉鏈法實現的,每一個Entry本質上是一個單向鏈表    transient Entry[] table;     // HashMap的大小,它是HashMap保存的鍵值對的數量    transient int size;     // HashMap的閾值,用于判斷是否需要調整HashMap的容量(threshold = 容量*加載因子)    int threshold;     // 加載因子實際大小    final float loadFactor;     // HashMap被改變的次數    transient volatile int modCount;

通過以上源碼可以看到在源碼中定義了一下幾個常量:

  • 默認加載因子:這東西說白了就是用來劃分整個HashMap容量的百分比,這里默認0.75就是說占用總容量的75%
  • 默認初始容量:如果你不在構造函數中傳值,new一個HashMap,他的容量就是2的4次方(16),并且增長也得是2的整數次方(冪)
  • 閥值:首先這個值等于默認加載因子和初始容量的乘機;他的作用是用來預警的,如果HashMap中的容量超過這個閥值了,那就會執行擴容操作,低于則沒事

很多人忽視的加載因子Load Factor

加載因子存在的原因,還是因為減緩哈希沖突,如果初始桶為16,等到滿16個元素才擴容,某些桶里可能就有不止一個元素了。所以加載因子默認為0.75,也就是說大小為16的HashMap,到了第13個元素,就會擴容成32。

2.1 考慮加載因子地設定初始大小

相比擴容時只是System.arraycopy()的ArrayList,HashMap擴容的代價其實蠻大的,首先,要生成一個新的桶數組,然后要把所有元素都重新Hash落桶一次,幾乎等于重新執行了一次所有元素的put。

所以如果你心目中有明確的Map 大小,設定時一定要考慮加載因子的存在。

建議你在知道你要存儲的容量的時候,直接這樣定義:

Map mapBest = new HashMap((int) ((float) 擬存的元素個數 / 0.75F + 1.0F));

這樣一次到位,雖然存在些資源浪費,但是比起重新擴容還是效率高很多

Map map = new HashMap(srcMap.size())這樣的寫法肯定是不對的,有25%的可能會遇上擴容。

Thrift里的做法比較粗暴, Map map = new HashMap( 2* srcMap.size()), 直接兩倍又有點浪費空間。

Guava的做法則是加上如下計算

(int) ((float) expectedSize / 0.75F + 1.0F);

2.2 減小加載因子

在構造函數里,設定加載因子是0.5甚至0.25。
如果你的Map是一個長期存在而不是每次動態生成的,而里面的key又是沒法預估的,那可以適當加大初始大小,同時減少加載因子,降低沖突的機率。畢竟如果是長期存在的map,浪費點數組大小不算啥,降低沖突概率,減少比較的次數更重要。

3. Key的設計

對于String型的Key,如果無法保證無沖突而且能用==來對比,那就盡量搞短點,否則一個個字符的equals還是花時間的。

甚至,對于已知的預定義Key,可以自己試著放一下,看沖不沖突。比如,像”a1”,”a2”,”a3” 這種,hashCode是個小數字遞增,絕對是不沖突的:)

看一下獲取key對應value的源碼

// 獲取key對應的value    public V get(Object key) {        if (key == null)            return getForNullKey();        // 獲取key的hash值        int hash = hash(key.hashCode());        // 在“該hash值對應的鏈表”上查找“鍵值等于key”的元素        for (Entry<K,V> e = table[indexFor(hash, table.length)];             e != null;             e = e.next) {            Object k;            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))                return e.value;        }        return null;    }

由源碼可知,如果hashCode 不沖突,那查找效率很高,但是如果hashCode一旦沖突,叫調用equals一個字節一個自己的去比較

  • 所以你把key設計的盡量短,一旦沖突也會少用點時間
  • 建議采用String,Integer 這樣的類作為鍵,原因如下:

特別是String,他是不可變的,也是final的,而且已經重寫了equals 和hashCode 方法,這個和HashMap 要求的計算hashCode的不可變性要求不謀而合,核心思想就是保證鍵值的唯一性,不變性,

其次是不可變性還有諸如線程安全的問題,以上這么定義鍵,可以最大限度的減少碰撞的出現

轉載整理自

https://blog.csdn.net/wjsshhx/article/details/67644254

http://www.importnew.com/21429.html

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容