【jdk源碼3】HashMap源碼學習

  能夠絕不誇張的說,HashMap是容器類中用的最頻繁的一個,而Java也對它進行優化,在jdk1.7及之前,當將相同Hash值的對象以key的身份放到HashMap中,HashMap的性能將由O(1)降低到O(N),因此jdk1.8將相同Hash值的key以紅黑樹的形式進行存儲。數據結構

 

1、簡單理解

1.1 初始容量的設計

  給個人感覺是,給用戶自由,可是要在限定的範圍內。ide

  首先介紹初始容量是什麼,引用Java API中的介紹:性能

HashMap 的實例有兩個參數影響其性能:初始容量 和加載因子。容量 是哈希表中桶的數量,初始容量只是哈希表在建立時的容量。加載因子 是哈希表在其容量自動增長以前能夠達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。

  也就是說,咱們能夠有遠見知道HashMap中將要存入多少數據,而相應的設置初始容量,減小rehash的次數,由於每次rehash將會對HashMap進行一次重構,影響性能,所以HashMap的構造方法中提供了對初始容量的設置:優化

HashMap(int initialCapacity) :構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。 
HashMap(int initialCapacity, float loadFactor) :構造一個帶指定初始容量和加載因子的空 HashMap。 

  可是HashMap在設計的時候,已經考慮到要rehash,以及根據Hash值在固定數量的桶中查詢數據,加上對數字的移位運算最高效,因此桶的數量被設計爲2的幾回方,可是用戶輸入初始容量是任意的,HashMap是怎麼處理的呢?它是取比輸入值-1大的且最近的2的幾回方的值:this

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

  也就是說最終初始容量不必定安裝你輸入的來,就像一開始說的那樣,HashMap給你必定的自由,可是不是絕對的自由。spa

  之後寫代碼也能夠這麼作,當一個重要屬性可能影響效率時,不能給使用者太大的自由度,否則用起來慢就很差了。設計

1.2 列表轉樹的條件

  首先簡單說明下列表和紅黑樹,對它們有了基本的認識後才能知道爲何決定將列表轉成紅黑樹:code

  • 所佔空間:紅黑樹>列表
  • 性能:紅黑樹>列表,可是當元素數量特別少時,列表的性能仍是大於紅黑樹的

  也就是,有一個閾值,當超過閾值時纔會將列表轉換成紅黑樹,爲了提升性能HashMap仍是考慮的不少的,主要有三個屬性涉及到列表和紅黑樹轉換:對象

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64

  TREEIFY_THRESHOLD :當一個節點的數量大於此數量的時候,將會將此節點列表轉換成一個樹。blog

  UNTREEIFY_THRESHOLD :當已是一個樹的節點,在移除元素的時候,若是移除後小於這個值,則將樹轉換成列表。

  MIN_TREEIFY_CAPACITY :當一個節點準備轉換成樹以前,若是HashMap桶的數量小於此,則不進行轉換,而是將HashMap進行擴容。

   也就是說爲了一個優化,至關於將HashMap一半的代碼進行了從新,爲了提高當元素都放置到一個桶時性能的降低。可是對於用戶來講是透明的,用戶在使用上徹底感覺不到變化,因此說優化是沒有終點的,這一點我仍是挺佩服他們的。

1.3 元素的大小比較

  以前講TreeMap的時候說過,放入TreeMap的key必須具有可比較性,要麼自己實現Comparable接口,要麼傳入key的比較器,由於若是key不能比較大小,就沒辦法構建一棵樹。而咱們在放入HashMap的key時,卻沒有對此有要求,它是怎麼實現的呢。

  首先判斷key的類型是不是Comparable,若是是,就經過自身的比較方法進行比較。

  若是key不是Comparable,那麼就經過key自己的Hash值進行比較,即使子類重寫了hashCode方法,也會用最原始的,其實是用了System的一個方法:

System.identityHashCode(Object x) 

  這個方法,返回給定對象的哈希碼,該代碼與默認的方法 hashCode() 返回的代碼同樣,不管給定對象的類是否重寫 hashCode()。

  也就是說最終老是能比較出大小,固然若是還同樣,說明key是同樣的,覆蓋便可。

  其中判斷key的類型是不是Comparable中有一段代碼以下:

static Class<?> comparableClassFor(Object x) {
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        if ((ts = c.getGenericInterfaces()) != null) {
            for (int i = 0; i < ts.length; ++i) {
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                     Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
        }
    }
    return null;
}

  說實話我一開始沒有想到會這麼複雜,後來我認真研究了一下發現一個類即使實現了Comparable接口,也有可能比較的是其餘類:

class Dog implements Comparable<Object>{
  public String name;

  public Dog(String name) {
    this.name = name;
  }
  @Override
  public int compareTo(Object o) {
    // TODO Auto-generated method stub
    return 0;
  }
}

  沒想到判斷的這麼嚴謹,由於印象中不多有類實現Comparable接口,而不去比較本身的,簡單總結它的邏輯:

  • 首先是一個Comparable
  • Comparable必須是一個參數化類型,就是說指定比較類型
  • 參數化有一個參數,且是它自己

  看完這部分,我想了想TreeMap爲何沒有借鑑HashMap的這種方式,而必須讓key具備比較性,緣由其實很簡單,HashMap的做用就是存儲快速讀取,而TreeMap的額外多了個目的就是排序,若是一個key不具有可比較性,而最終使用了最原始的hashCode,那排序就沒有了意義,還不如使用更高效的HashMap呢。

2、問題及總結

  HashMap作爲最經常使用的容器類,Java已經封裝的足夠好了,而咱們使用的時候若是能作到如下兩點也就能最大化的提升HashMap的性能:

  • 自定義對象作爲key時,重寫hashCode和equals方法
  • 若是能預見存入HashMap元素的數量,在初始化的時候指定

  其它暫時沒有遇到什麼問題。

相關文章
相關標籤/搜索