1,hashMap

1,前述

很早之前就想寫博客,一直想找個平臺,github的自定義博客不賴,有些難度自己也沒時間來研究,既然簡書很好那就從簡書開始吧

還有一直想研究個東西,jdk源碼,雖然是java出身,對前景一直有迷惑,不知道該干嘛,想想數(shù)據(jù)結(jié)構(gòu)和算法是重要的,并發(fā)又是很重要的一塊,那就從研究ConcurrentHashMap開始吧

public void main(){
    System.out.print("hello word !");
}

2,map接口

public interface Map<K,V> {
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    V put(K key, V value);
    V remove(Object key);
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();
    interface Entry<K,V> {
        K getKey();
        V getValue();
        V setValue(V value);
        boolean equals(Object o);
        int hashCode();
    }
    boolean equals(Object o);
    int hashCode();
}

上面列出的是jdk6中的interface內(nèi)容,jdk8新增了一些方法,jdk8開始加入了default修飾符,可以在interface中寫入具體的實現(xiàn)方法,在實現(xiàn)類中可以選擇性決定方法的實現(xiàn),本文還是以jdk8 中的內(nèi)容來講解,重點方法put () resize(),其他方法都是圍繞此來展開

3,hashmap

3-1,屬性

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
{
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認初始化大小16
    static final int MAXIMUM_CAPACITY = 1 << 30;  //默認最大容量
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默認擴容因子
    transient Entry[] table;//真正的存儲結(jié)構(gòu)
    transient int size;//當前已經(jīng)存儲元素的個數(shù)
    int threshold; //判斷size是否需要擴容的閾值。也是當前的容量,可以在后面的resize方法分析出來
    final float loadFactor;  //擴容因子,擴容后的容量為 loadFactor * threshold
    transient volatile int modCount;  //map 修改的次數(shù),包括put 和 delete
}

transient 修飾符表示序列化時不序列化此部分, HashMap 中的存儲數(shù)據(jù)的是數(shù)組,其中沒有被使用到的空間被序列化沒有意義。所以需要手動使用 writeObject() 方法,只序列化實際存儲元素的數(shù)組
默認的參數(shù)表示如果用戶不在構(gòu)造器中設(shè)置則以默認為準

3-2構(gòu)造

public HashMap(int initialCapacity, float loadFactor) {}
public HashMap(int initialCapacity){}
public HashMap(){}
public HashMap(Map<? extends K, ? extends V> m){}

/*
     列舉兩個范例
*/
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)  //不能超過最大值
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor; //擴容
    this.threshold = tableSizeFor(initialCapacity);
}
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

所有的構(gòu)造器都只是對屬性進行了賦值,但構(gòu)造器并沒有初始化 table ,這一操作在resize 方法中
loadFactor 擴容比例,如果用戶不指定則以默認為準
threshold 是擴容的閥值,有時候和當前數(shù)組的容量一樣,具體細節(jié)會在resize方法中分析出來,現(xiàn)在只是簡單介紹下,分為幾個情況:
如果構(gòu)造中輸入了initialCapacity參數(shù),會在構(gòu)造中通過tableSizeFor()計算得出,在resize方法執(zhí)行時,這個值就是數(shù)組的容量,所以要保證2的次冪特性;
如果構(gòu)造器中沒有輸入initialCapacity參數(shù),會在resize第一次執(zhí)行擴容時容量初始化為16,threshold 初始化為 12,

其中構(gòu)造中的計算方法如下

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;
}

tableSizeFor方法是保證函數(shù)返回值是大于等于參數(shù) initialCapacity 最小的2的冪次方的數(shù)值,比如initialCapacity是20,返回值32(2的5次冪)

/*
    | 或運算
    |= 等價于 n = n | a
    >>> 無符號位的左移

    int n = cap - 1; 
    當前cap本身就是2的次冪時,如果不減一,最終結(jié)果會變成 cap * 2,不符合我們預期
    n |= n >>> 1;
    右移一位,即n的二進制最高位右移1位,再與n本身或操作,最終n的高1-2位都為1
    n |= n >>> 2;
    右移2位,即n的二進制最高1-2位右移2位,與n本身或操作,最終n的高1-4位都是1
    n |= n >>> 4;
    。。。最終n的高位1-8都是1
    n |= n >>> 8;
    。。。最終n的高位1-16都是1
    n |= n >>> 16;
    。。。最終n的高位1-32都是1

    這里可以舉個栗子,假設(shè)給定的cap的值為20
    二進制表示:0001 0011,最終執(zhí)行完的結(jié)果為 0001 1111,再加1 為 0010 0000 = 32
*/
為什么cap要保持為2的冪次方?

存儲數(shù)據(jù)的數(shù)組table的下標是由key的Hash值決定的。在HashMap存儲數(shù)據(jù)的時候,我們期望數(shù)據(jù)能夠均勻分布,以避免哈希沖突。自然而然我們就會想到去用%取余的操作來實現(xiàn)我們這一構(gòu)想。

這里要了解到一個知識:取余(%)操作中如果除數(shù)是2的冪次方則等同于與其除數(shù)減一的與(&)操作。(這樣做比直接取余操作效率要好)

如果newCap是2的次冪時,下面成立:
 index = e.hash & (newCap - 1) 
 等同于:
 index = e.hash % newCap

3-3 node

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

Node<K,V> 類是HashMap中的靜態(tài)內(nèi)部類,實現(xiàn)Map.Entry<K,V>接口。 是map中value值的載體,除了key鍵、value值之外,還有next節(jié)點,元素之間可以構(gòu)成單向鏈表。

/*
    table 是map的數(shù)組,是所有數(shù)據(jù)的載體,如果key 的hash值有沖突時,就以鏈表存在,node提供了這種支持
*/
transient Entry[] table;

HashMap存儲的數(shù)據(jù)結(jié)構(gòu):維護了一個數(shù)組,每個數(shù)組又維護了一個單向鏈表。之所以這么設(shè)計,考慮到遇到哈希沖突的時候,同index的value值就用單向鏈表來維護。

3-4 hash方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/*
     index = e.hash & (newCap - 1) 
     ^ 異或操作,一樣則為0
*/

因為hash 的值最終要和 newCap-1 的值進行與操作,而 newCap 是2的次冪,減一后高位全為0,與操作時 hash 的高位就沒有參與進來,index 沖突的幾率會上升,h >>> 16 是將高位也參與到hash 中來能夠降低 index 沖突的幾率。(這是參考其他人的說法,個人不知道為何會降低)

3-5 put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false,  true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        // tab 為空,調(diào)用resize()初始化tab。
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 通過hash和key確定要存儲在table中的下標,如果沒有被占用,將value封裝為Node存儲
        tab[i] = newNode(hash, key, value, null);
    else {
        //當前key獲取的下標位置已經(jīng)被占用,可能需要用鏈表形式存儲
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // p為數(shù)組的第一個元素,如果key相同,value值應該直接被覆蓋,此時e = p為了在后面的方法中對e進行操作
            e = p;
        else if (p instanceof TreeNode)
            // 如果p是紅黑樹類型,調(diào)用putTreeVal方式賦值
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // index 相同的情況下,但key不同,可能繼續(xù)以鏈表形式存放或者轉(zhuǎn)為紅黑樹存放
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // 如果p的next為空,將新的value值添加至鏈表后面
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 如果鏈表長度大于8,鏈表轉(zhuǎn)化為紅黑樹,執(zhí)行插入
                        treeifyBin(tab, hash);
                    break;
                }
                // key相同則跳出循環(huán),key相同時,會繼續(xù)判斷是否將老的值進行替換
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            //根據(jù)規(guī)則選擇是否覆蓋value
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); //看源碼注釋好像是為LinkedHashMap準備的
            return oldValue;
        }
    }
    ++modCount; //map被修改的次數(shù)
    if (++size > threshold)
        // size大于加載因子,擴容
        resize();
    afterNodeInsertion(evict);  //看源碼注釋好像是為LinkedHashMap準備的
    return null;
}

注釋寫的已經(jīng)很好了,不需要再解釋

3-6 resize方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//舊table 大小
    int oldThr = threshold;
    int newCap, newThr = 0;
    //分為2種情況,1是table已經(jīng)存在,2是table 不存在需要初始化
    //初始化分為兩種:1threshold > 0 即初始化為自定義的長度,2反之則初始化為默認大小 (這里和構(gòu)造中有關(guān))
    if (oldCap > 0) {
        // table已存在
        if (oldCap >= MAXIMUM_CAPACITY) {
            // oldCap大于MAXIMUM_CAPACITY,threshold設(shè)置為int的最大值,并且不再繼續(xù)擴容,新的元素放到鏈表或樹
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //newCap設(shè)置為oldCap的2倍并小于MAXIMUM_CAPACITY,且大于默認值, 新的threshold增加為原來的2倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        // threshold>0, 將threshold設(shè)置為newCap,所以要用tableSizeFor方法保證threshold是2的冪次方
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 默認初始化,cap為16,threshold為12。除此之外 threshold 等于 數(shù)組的容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // newThr為0,newThr = newCap * 0.75
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //當擴容后,則在現(xiàn)在將容器容量重新賦值給擴容閥值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        // 新生成一個table數(shù)組
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // oldTab 復制到 newTab
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                   // 鏈表只有一個節(jié)點,直接賦值
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // e為紅黑樹的情況
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
擴容閥值和數(shù)組容量的關(guān)系

擴容分為兩種情況:1是數(shù)組還不存在時需要對數(shù)組進行初始化,2是數(shù)組已經(jīng)存在進行初始化

數(shù)組不存在時:
1,threshold>0, 將threshold設(shè)置為newCap也就是數(shù)組的大小,所以要用tableSizeFor方法保證threshold是2的冪次方
2,threshold = 0,將執(zhí)行默認初始化數(shù)組長度為16,擴容閥值為12
數(shù)組已經(jīng)存在時:
1,長度已經(jīng)超過最大值,不再進行擴容,擴容閥值設(shè)置為integer的最大值,新增的元素只能以鏈表或tree的方式存放
2,長度沒有超過最大值,則將長度設(shè)置為原有的2倍,如果擴大2倍后還沒有超過數(shù)組最大值,則擴容閥值也擴大2倍

這里有個細節(jié):如果構(gòu)造器中對threshold進行了賦值,那么數(shù)組的容量和閥值一樣,如果沒有賦值,閥值默認為12之后擴容時閥值也會變?yōu)橹暗?倍,但和數(shù)組容量并不一致

3-7remove(key) 方法 & remove(key, value) 方法

default boolean remove(Object key, Object value) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, value) ||
        (curValue == null && !containsKey(key))) {
        return false;
    }
    remove(key);
    return true;
}
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

這兩個方法最終都調(diào)用了removeNode方法

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //判斷tab不為null,長度大于0,hash 對應的數(shù)組元素 也不為null 才可以進行刪除
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // index 元素只有一個元素,node 是需要刪除的節(jié)點,先找到再后面進行刪除
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                // index處是一個紅黑樹,則在紅黑樹中找到需要刪除的node
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // index處是一個鏈表,遍歷鏈表尋找 node
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    //此處p的值已經(jīng)不是鏈表的第一個元素,是需要刪除node 的上一個元素
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 分不同情形刪除節(jié)點
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                //紅黑樹下,在紅黑樹中去刪除
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                //如果是第一個元素直接用next替換掉第一個
                tab[index] = node.next;
            else
                //如果不是第一個,則把上一個的next 直接指向下一個元素,p的元素在執(zhí)行到這一行時,已經(jīng)不是第一個元素
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

注釋寫的很清楚,不需要再進行解釋,這里紅黑樹的加入增加了處理的難度,但在目前的分析中有關(guān)treenode的部分,都有特定的方法,可以暫時不用分析也能看懂大概的邏輯,有關(guān)treenode部分,再下一節(jié)中介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 繼續(xù)深入Java基礎(chǔ)系列。今天是研究下哈希表,畢竟我們很多應用層的查找存儲框架都是哈希作為它的根數(shù)據(jù)結(jié)構(gòu)進行封裝的...
    JackFrost_fuzhu閱讀 1,340評論 0 4
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,029評論 25 709
  • 寫在讀完《人間失格》之后。--- 某石 ***************** 所謂人間的失格,大概是:源于自卑,而惡...
    瀾南潛石閱讀 263評論 0 1
  • 我喜歡你打哈欠的聲音 所以慢慢學會了你的哈欠 就在剛剛 我打了一個哈欠 心頭撞得震顫
    王不煩閱讀 214評論 0 1
  • 默子江/文 喧囂聲 零零碎碎 寒風瑟瑟的街角 又出現(xiàn)了你的那雙跛腳 陶瓷花盆落地的那一瞬間 我心碎 朝陽跨過田...
    默子江閱讀 145評論 0 0

友情鏈接更多精彩內(nèi)容