成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

Java集合之HashMap源碼解析

lindroid / 2927人閱讀

摘要:之前,其內(nèi)部是由數(shù)組鏈表來(lái)實(shí)現(xiàn)的,而對(duì)于鏈表長(zhǎng)度超過(guò)的鏈表將轉(zhuǎn)儲(chǔ)為紅黑樹(shù)。非線程安全,即任一時(shí)刻可以有多個(gè)線程同時(shí)寫,可能會(huì)導(dǎo)致數(shù)據(jù)的不一致。有時(shí)兩個(gè)會(huì)定位到相同的位置,表示發(fā)生了碰撞。

原文地址

HashMap

HashMapMap 的一個(gè)實(shí)現(xiàn)類,它代表的是一種鍵值對(duì)的數(shù)據(jù)存儲(chǔ)形式。

大多數(shù)情況下可以直接定位到它的值,因而具有很快的訪問(wèn)速度,但遍歷順序卻是不確定的。

HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。不保證有序(比如插入的順序)、也不保證序不隨時(shí)間變化。

jdk 8 之前,其內(nèi)部是由數(shù)組+鏈表來(lái)實(shí)現(xiàn)的,而 jdk 8 對(duì)于鏈表長(zhǎng)度超過(guò) 8 的鏈表將轉(zhuǎn)儲(chǔ)為紅黑樹(shù)。

HashMap非線程安全,即任一時(shí)刻可以有多個(gè)線程同時(shí)寫HashMap,可能會(huì)導(dǎo)致數(shù)據(jù)的不一致。如果需要滿足線程安全,可以用 CollectionssynchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap。

下面我們先來(lái)看一下HashMap內(nèi)部所用到的存儲(chǔ)結(jié)構(gòu)

HashMap數(shù)組+鏈表+紅黑樹(shù)(JDK1.8增加了紅黑樹(shù)部分)實(shí)現(xiàn)的

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

    Node(int hash, K key, V value, Node 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;
    }
}

NodeHashMap的一個(gè)內(nèi)部類,實(shí)現(xiàn)了Map.Entry接口,本質(zhì)上就是一個(gè)映射(鍵值對(duì))。

有時(shí)兩個(gè)key會(huì)定位到相同的位置,表示發(fā)生了Hash碰撞。當(dāng)然Hash算法計(jì)算結(jié)果越分散均勻,Hash碰撞的概率就越小,map的存取效率就會(huì)越高。

HashMap類中有一個(gè)非常重要的字段,就是 Node[] table,即哈希桶數(shù)組。

如果哈希桶數(shù)組很大,即使較差的Hash算法也會(huì)比較分散,如果哈希桶數(shù)組數(shù)組很小,即使好的Hash算法也會(huì)出現(xiàn)較多碰撞。

所以就需要在空間成本和時(shí)間成本之間權(quán)衡,其實(shí)就是在根據(jù)實(shí)際情況確定哈希桶數(shù)組的大小,并在此基礎(chǔ)上設(shè)計(jì)好的hash算法減少Hash碰撞。那么通過(guò)什么方式來(lái)控制map使得Hash碰撞的概率又小,哈希桶數(shù)組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴(kuò)容機(jī)制。

下面我們就來(lái)看一下hashmap中經(jīng)過(guò)jdk1.8優(yōu)化過(guò)的Hash算法和擴(kuò)容機(jī)制。

不過(guò)在這之前我們先了解下hashmap中的變量

//初始化容量16 hashMap的容量必須是2的指數(shù)倍,Hashtable是11
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認(rèn)加載因子默認(rèn)的平衡因子為0.75,這是權(quán)衡了時(shí)間復(fù)雜度與空間復(fù)雜度之后的最好取值
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 如果鏈表的長(zhǎng)度超過(guò)這個(gè)閾值就改用紅黑樹(shù)存儲(chǔ)
static final int TREEIFY_THRESHOLD = 8;

static final int UNTREEIFY_THRESHOLD = 6;

static final int MIN_TREEIFY_CAPACITY = 64;

transient Node[] table;

transient Set> entrySet;

transient int size;    //實(shí)際存儲(chǔ)的鍵值對(duì)個(gè)數(shù)

transient int modCount;

 //閾值,當(dāng)table == {}時(shí),該值為初始容量(初始容量默認(rèn)為16);當(dāng)table被填充了,也就是為table分配內(nèi)存空間后,threshold一般為 capacity*loadFactory。
int threshold;  

final float loadFactor;    //負(fù)載因子,代表了table的填充度有多少,默認(rèn)是0.75

在HashMap中有兩個(gè)很重要的參數(shù),容量(Capacity)和負(fù)載因子(Load factor)

Capacity就是buckets的數(shù)目,Load factor就是buckets填滿程度的最大比例。如果對(duì)迭代性能要求很高的話不要把capacity設(shè)置過(guò)大,也不要把load factor設(shè)置過(guò)小。當(dāng)bucket填充的數(shù)目(即hashmap中元素的個(gè)數(shù))大于capacity*load factor時(shí)就需要調(diào)整buckets的數(shù)目為當(dāng)前的2倍。

Hash算法
static final int hash(Object key) {
    int h;
    // h = key.hashCode() 為第一步 取hashCode值
    // h ^ (h >>> 16)  為第二步 高位參與運(yùn)算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

static int indexFor(int h, int length) {  
     return h & (length-1);  //第三步 取模運(yùn)算
}

indexFor是jdk1.7的源碼,jdk1.8沒(méi)有這個(gè)方法但是jdk1.8也是通過(guò)取模運(yùn)算來(lái)計(jì)算的

這里的Hash算法本質(zhì)上就是三步:取key的hashCode值、高位運(yùn)算、取模運(yùn)算。

對(duì)于任意給定的對(duì)象,只要它的hashCode()返回值相同,那么程序調(diào)用方法一所計(jì)算得到的Hash碼值總是相同的。我們首先想到的就是把hash值對(duì)數(shù)組長(zhǎng)度取模運(yùn)算,這樣一來(lái),元素的分布相對(duì)來(lái)說(shuō)是比較均勻的。但是,模運(yùn)算的消耗還是比較大的,這里我們用&位運(yùn)算來(lái)優(yōu)化效率。

這個(gè)方法非常巧妙,它通過(guò)h & (table.length -1)來(lái)得到該對(duì)象的保存位,而HashMap底層數(shù)組的長(zhǎng)度總是2的n次方,這是HashMap在速度上的優(yōu)化。當(dāng)length總是2的n次方時(shí),h& (length-1)運(yùn)算等價(jià)于對(duì)length取模,也就是h%length,但是&%具有更高的效率。

JDK1.8的實(shí)現(xiàn)中,優(yōu)化了高位運(yùn)算的算法,通過(guò)hashCode()的高16位異或低16位實(shí)現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質(zhì)量來(lái)考慮的,這么做可以Node數(shù)組table的length比較小的時(shí)候,也能保證考慮到高低Bit都參與到Hash的計(jì)算中,同時(shí)不會(huì)有太大的開(kāi)銷。

擴(kuò)容機(jī)制

擴(kuò)容(resize)就是重新計(jì)算容量,向HashMap對(duì)象里不停的添加元素,而HashMap對(duì)象內(nèi)部的數(shù)組無(wú)法裝載更多的元素時(shí),對(duì)象就需要擴(kuò)大數(shù)組的長(zhǎng)度,以便能裝入更多的元素。

當(dāng)然Java里的數(shù)組是無(wú)法自動(dòng)擴(kuò)容的,方法是使用一個(gè)新的數(shù)組代替已有的容量小的數(shù)組,就像我們用一個(gè)小桶裝水,如果想裝更多的水,就得換大水桶。

當(dāng)put時(shí),如果發(fā)現(xiàn)目前的bucket占用程度已經(jīng)超過(guò)了Load Factor所希望的比例,那么就會(huì)發(fā)生resize。在resize的過(guò)程,簡(jiǎn)單的說(shuō)就是把bucket擴(kuò)充為2倍,之后重新計(jì)算index,把節(jié)點(diǎn)再放到新的bucket中。
因?yàn)槲覀兪褂玫氖?次冪的擴(kuò)展(指長(zhǎng)度擴(kuò)為原來(lái)2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動(dòng)2次冪的位置。

例如我們從16擴(kuò)展為32時(shí),具體的變化如下所示:

因此元素在重新計(jì)算hash之后,因?yàn)閚變?yōu)?倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會(huì)發(fā)生這樣的變化:

因此,我們?cè)跀U(kuò)充HashMap的時(shí)候,不需要重新計(jì)算hash,只需要看看原來(lái)的hash值新增的那個(gè)bit是1還是0就好了,是0的話索引沒(méi)變,是1的話索引變成“原索引+oldCap”。

這個(gè)設(shè)計(jì)確實(shí)非常的巧妙,既省去了重新計(jì)算hash值的時(shí)間,而且同時(shí),由于新增的1bit是0還是1可以認(rèn)為是隨機(jī)的,因此resize的過(guò)程,均勻的把之前的沖突的節(jié)點(diǎn)分散到新的bucket了。

final Node[] resize() {
    Node[] oldTab = table;    
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {    //說(shuō)明舊數(shù)組已經(jīng)被初始化完成了,此處需要給舊數(shù)組擴(kuò)容   
        if (oldCap >= MAXIMUM_CAPACITY) {    如果容量超過(guò)Hash Map限定的最大值,將不再擴(kuò)容
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }    // 沒(méi)超過(guò)最大值,就擴(kuò)充為原來(lái)的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;   // 2倍
    }
    //數(shù)組未初始化,但閾值不為 0,為什么不為 0 ?
    //構(gòu)造函數(shù)根據(jù)傳入的容量打造了一個(gè)合適的數(shù)組容量暫存在閾值中,這里直接使用
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {    //數(shù)組未初始化并且閾值也為0,說(shuō)明一切都以默認(rèn)值進(jìn)行構(gòu)造
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }    
    // newCap = oldThr 之后并沒(méi)有計(jì)算閾值,所以 newThr = 0
    // 重新計(jì)算下一次進(jìn)行擴(kuò)容的上限
    if (newThr == 0) {   
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node[] newTab = (Node[])new Node[newCap];  //根據(jù)新的容量初始化一個(gè)數(shù)組
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {     // 把每個(gè)bucket都移動(dòng)到新的buckets中
            Node e;
            if ((e = oldTab[j]) != null) {  //獲取頭結(jié)點(diǎn)
                oldTab[j] = null;
                if (e.next == null)   //說(shuō)明鏈表或者紅黑樹(shù)只有一個(gè)頭結(jié)點(diǎn),轉(zhuǎn)移至新表
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)  //如果 e 是紅黑樹(shù)結(jié)點(diǎn),紅黑樹(shù)分裂,轉(zhuǎn)移至新表
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                else {   //這部分是將鏈表中的各個(gè)節(jié)點(diǎn)原序地轉(zhuǎn)移至新表中
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {    // 原索引
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {                          // 原索引+oldCap
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {      // 原索引放到bucket里
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {     // 原索引+oldCap放到bucket里
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

下面我們?cè)賮?lái)看看hashmap中的其他方法

構(gòu)造函數(shù)
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);
}

這是一個(gè)最基本的構(gòu)造函數(shù),需要調(diào)用方傳入兩個(gè)參數(shù),initialCapacityloadFactor

程序的大部分代碼在判斷傳入?yún)?shù)的合法性,initialCapacity 小于零將拋出異常,大于 MAXIMUM_CAPACITY 將被限定為 MAXIMUM_CAPACITY。loadFactor 如果小于等于零或者非數(shù)字類型也會(huì)拋出異常。

整個(gè)構(gòu)造函數(shù)的核心在對(duì) threshold 的初始化操作:

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

由以上代碼可以看出,當(dāng)在實(shí)例化HashMap實(shí)例時(shí),如果給定了initialCapacity,由于HashMapcapacity都是2的冪次方,因此這個(gè)方法用于找到大于等于initialCapacity的最小的2的冪(initialCapacity如果就是2的冪,則返回的還是這個(gè)數(shù))。

下面分析這個(gè)算法:

首先,我們想一下為什么要對(duì)cap做減1操作?

int n = cap - 1 

這是為了防止,cap已經(jīng)是2的冪。如果cap已經(jīng)是2的冪,又沒(méi)有執(zhí)行這個(gè)減1操作,則執(zhí)行完后面的幾條無(wú)符號(hào)右移操作之后,返回的capacity將是這個(gè)cap的2倍。如果不懂,要看完后面的幾個(gè)無(wú)符號(hào)右移之后再回來(lái)看看。

下面看看這幾個(gè)無(wú)符號(hào)右移操作:

如果n這時(shí)為0了(經(jīng)過(guò)了cap-1之后),則經(jīng)過(guò)后面的幾次無(wú)符號(hào)右移依然是0,最后返回的capacity是1(最后有個(gè)n+1的操作)。

這里我們只討論n不等于0的情況。

n |= n >>> 1;

由于n不等于0,則n的二進(jìn)制表示中總會(huì)有一bit為1,這時(shí)考慮最高位的1。通過(guò)無(wú)符號(hào)右移1位,則將最高位的1右移了1位,再做或操作,使得n的二進(jìn)制表示中與最高位的1緊鄰的右邊一位也為1,如000011xxxxxx。

n |= n >>> 2;

注意,這個(gè)n已經(jīng)進(jìn)行過(guò) n |= n >>> 1; 操作。假設(shè)此時(shí)n為000011xxxxxx ,則n無(wú)符號(hào)右移兩位,會(huì)將最高位兩個(gè)連續(xù)的1右移兩位,然后再與原來(lái)的n做或操作,這樣n的二進(jìn)制表示的高位中會(huì)有4個(gè)連續(xù)的1。如00001111xxxxxx 。

n |= n >>> 4;

這次把已經(jīng)有的高位中的連續(xù)的4個(gè)1,右移4位,再做或操作,這樣n的二進(jìn)制表示的高位中會(huì)有8個(gè)連續(xù)的1。如00001111 1111xxxxxx 。

以此類推 。。。

注意,容量最大也就是32bit的正數(shù),因此最后 n |= n >>> 16; 最多也就32個(gè)1,但是這時(shí)已經(jīng)大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。

下面我們通過(guò)一個(gè)圖片來(lái)看一下整個(gè)過(guò)程:

HashMap 中還有很多的重載構(gòu)造函數(shù),但幾乎都是基于上述的構(gòu)造函數(shù)的。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

以上這些構(gòu)造函數(shù)都沒(méi)有直接的創(chuàng)建一個(gè)切實(shí)存在的數(shù)組,他們都是在為創(chuàng)建數(shù)組需要的一些參數(shù)做初始化,
所以有些在構(gòu)造函數(shù)中并沒(méi)有被初始化的屬性都會(huì)在實(shí)際初始化數(shù)組的時(shí)候用默認(rèn)值替換。

實(shí)際對(duì)數(shù)組進(jìn)行初始化是在添加元素的時(shí)候進(jìn)行的(即put方法)

public HashMap(Map m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
put方法

put 方法也是HashMap中比較重要的方法,因?yàn)橥ㄟ^(guò)該方法我們可以窺探到 HashMap 在內(nèi)部是如何進(jìn)行數(shù)據(jù)存儲(chǔ)的,所謂的數(shù)組+鏈表+紅黑樹(shù)的存儲(chǔ)結(jié)構(gòu)是如何形成的,又是在何種情況下將鏈表轉(zhuǎn)換成紅黑樹(shù)來(lái)優(yōu)化性能的。

put方法的大致實(shí)現(xiàn)過(guò)程如下:

對(duì)key的hashCode()做hash,然后再計(jì)算index;

如果沒(méi)碰撞直接放到bucket里;

如果碰撞了,以鏈表的形式存在buckets后;

如果碰撞導(dǎo)致鏈表過(guò)長(zhǎng)(大于等于TREEIFY_THRESHOLD),就把鏈表轉(zhuǎn)換成紅黑樹(shù);

如果節(jié)點(diǎn)已經(jīng)存在就替換old value(保證key的唯一性)

如果bucket滿了(超過(guò)load factor*current capacity),就要resize。

public V put(K key, V value) {    // 對(duì)key的hashCode()做hash
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)   // tab為空則創(chuàng)建(初次添加元素)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)   //根據(jù)鍵值key計(jì)算hash值得到插入的數(shù)組索引i,如果table[i]==null,直接新建節(jié)點(diǎn)添加 
        tab[i] = newNode(hash, key, value, null);
    else {   //如果對(duì)應(yīng)的節(jié)點(diǎn)存在元素
        Node e; K k;    
        if (p.hash == hash &&       //判斷table[i]的首個(gè)元素是否和key一樣,如果相同直接覆蓋value
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)   //判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹(shù),如果是紅黑樹(shù),則直接在樹(shù)中插入鍵值對(duì)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
        else {
        // 遍歷table[i],判斷鏈表長(zhǎng)度是否大于TREEIFY_THRESHOLD(默認(rèn)值為8),大于8的話把鏈表轉(zhuǎn)換為紅黑樹(shù),在紅黑樹(shù)中執(zhí)行插入操作,否則進(jìn)行鏈表的插入操作;
        // 遍歷過(guò)程中若發(fā)現(xiàn)key已經(jīng)存在直接覆蓋value即可;
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }  //e 不是 null,說(shuō)明當(dāng)前的 put 操作是一次修改操作并且e指向的就是需要被修改的結(jié)點(diǎn)
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 插入成功后,判斷實(shí)際存在的鍵值對(duì)數(shù)量size是否超多了最大容量threshold,如果超過(guò),進(jìn)行擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
get函數(shù)實(shí)現(xiàn)

在理解了put之后,get就很簡(jiǎn)單了。大致思路如下:

bucket里的第一個(gè)節(jié)點(diǎn),直接命中;
如果有沖突,則通過(guò)key.equals(k)去查找對(duì)應(yīng)的entry
若為樹(shù),則在樹(shù)中通過(guò)key.equals(k)查找,O(logn);
若為鏈表,則在鏈表中通過(guò)key.equals(k)查找,O(n)。

具體代碼的實(shí)現(xiàn)如下:

public V get(Object key) {
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 直接命中
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 未命中
        if ((e = first.next) != null) {
            // 在樹(shù)中g(shù)et
            if (first instanceof TreeNode)
                return ((TreeNode)first).getTreeNode(hash, key);
            // 在鏈表中g(shù)et
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
remove方法

刪除操作就是一個(gè)查找+刪除的過(guò)程,相對(duì)于添加操作其實(shí)容易一些

public V remove(Object key) {
    Node e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

根據(jù)鍵值刪除指定節(jié)點(diǎn),這是一個(gè)最常見(jiàn)的操作了。顯然,removeNode 方法是核心。

final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
    Node[] tab; Node p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {
            if (node instanceof TreeNode)                                                                     ((TreeNode)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

刪除操作需要保證在表不為空的情況下進(jìn)行,并且 p 節(jié)點(diǎn)根據(jù)鍵的 hash 值對(duì)應(yīng)到數(shù)組的索引,在該索引處必定有節(jié)點(diǎn),如果為 null ,那么間接說(shuō)明此鍵所對(duì)應(yīng)的結(jié)點(diǎn)并不存在于整個(gè) HashMap 中,這是不合法的,所以首先要在這兩個(gè)大前提下才能進(jìn)行刪除結(jié)點(diǎn)的操作。

第一步

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
     node = p;

需要?jiǎng)h除的結(jié)點(diǎn)就是這個(gè)頭節(jié)點(diǎn),讓 node 引用指向它。否則說(shuō)明待刪除的結(jié)點(diǎn)在當(dāng)前 p 所指向的頭節(jié)點(diǎn)的鏈表或紅黑樹(shù)中,于是需要我們遍歷查找。

第二步

else if ((e = p.next) != null) {
     if (p instanceof TreeNode)
          node = ((TreeNode)p).getTreeNode(hash, key);
     else {
         do {
              if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {
                     node = e;
              break;
         }
         p = e;
         } while ((e = e.next) != null);
     }
}

如果頭節(jié)點(diǎn)是紅黑樹(shù)結(jié)點(diǎn),那么調(diào)用紅黑樹(shù)自己的遍歷方法去得到這個(gè)待刪結(jié)點(diǎn)。否則就是普通鏈表,我們使用 do while 循環(huán)去遍歷找到待刪結(jié)點(diǎn)。找到節(jié)點(diǎn)之后,接下來(lái)就是刪除操作了。

第三步

if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {
       if (node instanceof TreeNode)
                    ((TreeNode)node).removeTreeNode(this, tab, movable);
       else if (node == p)
            tab[index] = node.next;
       else
            p.next = node.next;
       ++modCount;
       --size;
       afterNodeRemoval(node);
       return node;
 }

刪除操作也很簡(jiǎn)單,如果是紅黑樹(shù)結(jié)點(diǎn)的刪除,直接調(diào)用紅黑樹(shù)的刪除方法進(jìn)行刪除即可,如果是待刪結(jié)點(diǎn)就是一個(gè)頭節(jié)點(diǎn),那么用它的 next 結(jié)點(diǎn)頂替它作為頭節(jié)點(diǎn)存放在 table[index] 中,如果刪除的是普通鏈表中的一個(gè)節(jié)點(diǎn),用該結(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)直接跳過(guò)該待刪結(jié)點(diǎn)指向它的 next 結(jié)點(diǎn)即可。

最后,如果 removeNode 方法刪除成功將返回被刪結(jié)點(diǎn),否則返回 null。

其他常用方法 clear
public void clear() {
    Node[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

該方法調(diào)用結(jié)束后將清除 HashMap 中存儲(chǔ)的所有元素。

keySet
//實(shí)例屬性 keySet
transient volatile Set        keySet;

public Set keySet() {
    Set ks;
    return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}
final class KeySet extends AbstractSet {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator iterator()     { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
}

HashMap 中定義了一個(gè) keySet 的實(shí)例屬性,它保存的是整個(gè) HashMap 中所有鍵的集合。上述所列出的 KeySet 類是 Set 的一個(gè)實(shí)現(xiàn)類,它負(fù)責(zé)為我們提供有關(guān) HashMap 中所有對(duì)鍵的操作。

可以看到,KeySet 中的所有的實(shí)例方法都依賴當(dāng)前的 HashMap 實(shí)例,也就是說(shuō),我們對(duì)返回的 keySet 集中的任意一個(gè)操作都會(huì)直接映射到當(dāng)前 HashMap 實(shí)例中,例如你執(zhí)行刪除一個(gè)鍵的操作,那么 HashMap 中將會(huì)少一個(gè)節(jié)點(diǎn)。

values
public Collection values() {
    Collection vs;
    return (vs = values) == null ? (values = new Values()) : vs;
}

values 方法其實(shí)和 keySet 方法類似,它返回了所有節(jié)點(diǎn)的 value 屬性所構(gòu)成的 Collection 集合,此處不再贅述。

entrySet
public Set> entrySet() {
    Set> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

它返回的是所有節(jié)點(diǎn)的集合,或者說(shuō)是所有的鍵值對(duì)集合。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/69676.html

相關(guān)文章

  • java源碼

    摘要:集合源碼解析回歸基礎(chǔ),集合源碼解析系列,持續(xù)更新和源碼分析與是兩個(gè)常用的操作字符串的類。這里我們從源碼看下不同狀態(tài)都是怎么處理的。 Java 集合深入理解:ArrayList 回歸基礎(chǔ),Java 集合深入理解系列,持續(xù)更新~ JVM 源碼分析之 System.currentTimeMillis 及 nanoTime 原理詳解 JVM 源碼分析之 System.currentTimeMi...

    Freeman 評(píng)論0 收藏0
  • Java集合LinkedHashMap源碼解析

    摘要:底層基于拉鏈?zhǔn)降纳⒘薪Y(jié)構(gòu),并在中引入紅黑樹(shù)優(yōu)化過(guò)長(zhǎng)鏈表的問(wèn)題。在其之上,通過(guò)維護(hù)一條雙向鏈表,實(shí)現(xiàn)了散列數(shù)據(jù)結(jié)構(gòu)的有序遍歷。 原文地址 LinkedHashMap LinkedHashMap繼承自HashMap實(shí)現(xiàn)了Map接口?;緦?shí)現(xiàn)同HashMap一樣,不同之處在于LinkedHashMap保證了迭代的有序性。其內(nèi)部維護(hù)了一個(gè)雙向鏈表,解決了 HashMap不能隨時(shí)保持遍歷順序和插...

    QiShare 評(píng)論0 收藏0
  • Java相關(guān)

    摘要:本文是作者自己對(duì)中線程的狀態(tài)線程間協(xié)作相關(guān)使用的理解與總結(jié),不對(duì)之處,望指出,共勉。當(dāng)中的的數(shù)目而不是已占用的位置數(shù)大于集合番一文通版集合番一文通版垃圾回收機(jī)制講得很透徹,深入淺出。 一小時(shí)搞明白自定義注解 Annotation(注解)就是 Java 提供了一種元程序中的元素關(guān)聯(lián)任何信息和著任何元數(shù)據(jù)(metadata)的途徑和方法。Annotion(注解) 是一個(gè)接口,程序可以通過(guò)...

    wangtdgoodluck 評(píng)論0 收藏0
  • 我的阿里路+Java面經(jīng)考點(diǎn)

    摘要:我的是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)。因?yàn)槲倚睦砗芮宄?,我的目?biāo)是阿里。所以在收到阿里之后的那晚,我重新規(guī)劃了接下來(lái)的學(xué)習(xí)計(jì)劃,將我的短期目標(biāo)更新成拿下阿里轉(zhuǎn)正。 我的2017是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕JDK源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)offer。然后五月懷著忐忑的心情開(kāi)始了螞蟻金...

    姘擱『 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<