1. 程式人生 > >HashMap原始碼解析(基於JDK1.7)

HashMap原始碼解析(基於JDK1.7)

一、HashMap簡介

雜湊表(hash table)也叫散列表,是一種非常重要的資料結構,應用場景及其豐富,許多快取技術(比如memcached)的核心其實就是在記憶體中維護一張大的雜湊表,而HashMap的實現原理就是基於此。那麼什麼是雜湊表呢?
在討論雜湊表之前,我們先大概瞭解下其他資料結構在新增,查詢等基礎操作執行效能

  • 陣列:採用一段連續的儲存單元來儲存資料。對於指定下標的查詢,時間複雜度為O(1);通過給定值進行查詢,需要遍歷陣列,逐一比對給定關鍵字和陣列元素,時間複雜度為O(n),當然,對於有序陣列,則可採用二分查詢,插值查詢,斐波那契查詢等方式,可將查詢複雜度提高為O(logn);對於一般的插入刪除操作,涉及到陣列元素的移動,其平均複雜度也為O(n)。對應到集合實現,代表就是ArrayList。
  • 線性連結串列:對於連結串列的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度為O(1),而查詢操作需要遍歷連結串列逐一進行比對,複雜度為O(n)。對應的集合類是LinkedList。
  • 二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查詢,刪除等操作,平均複雜度均為O(logn)。對應的集合類有TreeSet和TreeMap。
  • 雜湊表:相比上述幾種資料結構,在雜湊表中進行新增,刪除,查詢等操作,效能十分之高,不考慮雜湊衝突的情況下,僅需一次定位即可完成,時間複雜度為O(1)。對應的集合類就是HashMap。

雜湊表的主幹就是陣列。我們要新增或查詢某個元素,我們通過把當前元素的關鍵字 通過某個函式對映到陣列中的某個位置,通過陣列下標一次定位就可完成操作。即:

儲存位置 = f(關鍵字)

其中,這個函式f一般稱為雜湊函式,這個函式的設計好壞會直接影響到雜湊表的優劣。這會涉及到雜湊衝突。

當我們對某個元素進行雜湊運算,得到一個儲存地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的雜湊衝突,也叫雜湊碰撞。前面我們提到過,雜湊函式的設計至關重要,好的雜湊函式會盡可能地保證計算簡單和雜湊地址分佈均勻。但是,我們需要清楚的是,陣列是一塊連續的固定長度的記憶體空間,再好的雜湊函式也不能保證得到的儲存地址絕對不發生衝突。那麼雜湊衝突如何解決呢?雜湊衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的儲存地址)、再雜湊函式法、鏈地址法。而HashMap即是採用了鏈地址法,也就是陣列+連結串列的方式。
簡單來說,HashMap由陣列+連結串列組成的,陣列是HashMap的主體,連結串列則是主要為了解決雜湊衝突而存在的,如果定位到的陣列位置不含連結串列(當前entry的next指向null),那麼對於查詢,新增等操作很快,僅需一次定址即可;如果定位到的陣列包含連結串列,對於新增操作,其時間複雜度依然為O(1),因為最新的Entry會插入連結串列頭部,急需要簡單改變引用鏈即可,而對於查詢操作來講,此時就需要遍歷連結串列,然後通過key物件的equals方法逐一比對查詢。所以,效能考慮,HashMap中的連結串列出現越少,效能才會越好。

二、HashMap的原始碼實現

1、儲存結構

HashMap的內部儲存結構其實是陣列和連結串列的結合。當例項化一個HashMap時,系統會建立一個長度為Capacity的Entry陣列,這個長度被稱為容量(Capacity),在這個陣列中可以存放元素的位置我們稱之為“桶”(bucket),每個bucket都有自己的索引,系統可以根據索引快速的查詢bucket中的元素。 每個bucket中儲存一個元素,即一個Entry物件,但每一個Entry物件可以帶一個引用變數,用於指向下一個元素,因此,在一個桶中,就有可能生成一個Entry鏈。 Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。 Entry是HashMap中的一個靜態內部類。程式碼如下:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//儲存指向下一個Entry的引用,單鏈表結構
        int hash;//對key的hashcode值進行hash運算後得到的值,儲存在Entry,避免重複計算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

經過以上分析,HashMap的儲存結構圖如下:
這裡寫圖片描述
這裡寫圖片描述
一個長度為16的陣列中,每個元素儲存的是一個連結串列的頭結點。那麼這些元素是按照什麼樣的規則儲存到陣列中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的雜湊值對陣列長度取模得到。比如上述雜湊表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都儲存在陣列下標為12的位置。

在儲存一對值時(Key—->Value對),實際上是儲存在一個Entry的物件e中,程式通過key計算出Entry物件的儲存位置。換句話說,Key—->Value的對應關係是通過key—-Entry—-value這個過程實現的,所以就有我們表面上知道的key存在哪裡,value就存在哪裡。

2、構造方法

先看HashMap中的幾個重要屬性:

//預設初始化化容量,即16  
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

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

//預設裝載因子  
static final float DEFAULT_LOAD_FACTOR = 0.75f;  

//HashMap內部的儲存結構是一個數組,此處陣列為空,即沒有初始化之前的狀態  
static final Entry<?,?>[] EMPTY_TABLE = {};  

//空的儲存實體  
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  

//實際儲存的key-value鍵值對的個數
transient int size;

//閾值,當table == {}時,該值為初始容量(初始容量預設為16);當table被填充了,也就是為table分配記憶體空間後,threshold一般為 capacity*loadFactory。HashMap在進行擴容時需要參考threshold
int threshold;

//負載因子,代表了table的填充度有多少,預設是0.75
final float loadFactor;

//用於快速失敗,由於HashMap非執行緒安全,在對HashMap進行迭代時,如果期間其他執行緒的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要丟擲異常ConcurrentModificationException
transient int modCount;

//預設的threshold值  
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

HashMap有4個構造器,其他構造器如果使用者沒有傳入initialCapacity 和loadFactor這兩個引數,會使用預設值。initialCapacity預設為16,loadFactory預設為0.75。

 //計算Hash值時的key  
    transient int hashSeed = 0;  

    //通過初始容量和狀態因子構造HashMap  
    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;  
        threshold = initialCapacity;  
        init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
    }  

    //通過擴容因子構造HashMap,容量去預設值,即16  
    public HashMap(int initialCapacity) {  
        this(initialCapacity, DEFAULT_LOAD_FACTOR);  
    }  

    //裝載因子取0.75,容量取16,構造HashMap  
    public HashMap() {  
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  
    }  

    //通過其他Map來初始化HashMap,容量通過其他Map的size來計算,裝載因子取0.75  
    public HashMap(Map<? extends K, ? extends V> m) {  
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  
        inflateTable(threshold);//初始化HashMap底層的陣列結構  
        putAllForCreate(m);//新增m中的元素  
    }  

從上面這段程式碼我們可以看出,在常規構造器中,並沒有馬上為陣列table分配記憶體空間(有一個入參為指定Map的構造器例外),事實上是在執行第一次put操作的時候才真正構建table陣列。

3、put操作

如果兩個key通過hash%Entry[].length得到的index相同,會不會有覆蓋的危險?為了解決這個問題,HashMap裡面用到鏈式資料結構的一個概念。上面我們提到過Entry類裡面有一個next屬性,作用是指向下一個Entry。打個比方, 第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會後又進來一個鍵值對B,通過計算其index也等於0,現在怎麼辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性連結在一起。所以疑問不用擔心。也就是說陣列中儲存的是最後插入的元素。到這裡為止,HashMap的大致實現,我們應該已經清楚了。

public V put(K key, V value) {
        //如果table陣列為空陣列{},進行陣列填充(為table分配實際記憶體空間),入參為threshold,此時threshold為initialCapacity 預設是1<<4(=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//分配陣列空間
        }
       //如果key為null,儲存位置為table[0]或table[0]的衝突鏈上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//對key的hashcode進一步計算,確保雜湊均勻
        int i = indexFor(hash, table.length);//獲取在table中的實際位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果該對應資料已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);//呼叫value的回撥函式,其實這個函式也為空實現
                return oldValue;
            }
        }
        modCount++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗
        addEntry(hash, key, value, i);//新增一個entry
        return null;
    }

inflateTable的原始碼如下:

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處為threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大於1
        table = new Entry[capacity];//分配空間
        initHashSeedAsNeeded(capacity);//選擇合適的Hash因子
    }

inflateTable這個方法用於為主幹陣列table在記憶體中分配儲存空間,通過roundUpToPowerOf2(toSize)可以確保capacity為大於或等於toSize的最接近toSize的二次冪,比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。其實現如下:

private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

roundUpToPowerOf2中的這段處理使得陣列長度一定為2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其他bit位為0)所代表的數值。

在對陣列進行空間分配後,會根據hash函式計算雜湊值,其實現如下:

//用了很多的異或,移位等運算,對key的hashcode進一步進行計算以及二進位制位的調整等來保證最終獲取的儲存位置儘量分佈均勻
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {//這裡針對String優化了Hash函式,是否使用新的Hash函式和Hash因子有關  
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

從上面的操作看以看出,影響HashMap元素的儲存位置的只有key的值,與value值無關。

通過hash函式得到雜湊值後,再通過indexFor進一步處理來獲取實際的儲存位置,其實現如下:

    //返回陣列下標
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

h&(length-1)保證獲取的index一定在陣列範圍內,舉個例子,預設容量16,length-1=15,h=18,轉換成二進位制計算為
這裡寫圖片描述
最終計算出的index=2。有些版本的對於此處的計算會使用 取模運算,也能保證index一定在陣列範圍內,不過位運算對計算機來說,效能更高一些(HashMap中有大量位運算)。
通過以上分析,我們看到,要得到一個元素的儲存位置,需要如下幾步:
1、獲取該元素的key值
2、通過hash方法得到key的雜湊值,這其中需要用到key的hashcode值。
3、通過indexFor計算得到儲存的下標位置。

最後,得到儲存的下標位置後,我們就可以將元素放入HashMap中,具體通過addEntry實現:

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生雜湊衝突時進行擴容,新容量為舊容量的2倍
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);//擴容後重新計算插入的位置下標
        }

        //把元素放入HashMap的桶的對應位置
        createEntry(hash, key, value, bucketIndex);
    }
//建立元素  
    void createEntry(int hash, K key, V value, int bucketIndex) {  
        Entry<K,V> e = table[bucketIndex];  //獲取待插入位置元素
        table[bucketIndex] = new Entry<>(hash, key, value, e);//這裡執行連結操作,使得新插入的元素指向原有元素。
//這保證了新插入的元素總是在連結串列的頭  
        size++;//元素個數+1  
    }  

通過以上程式碼能夠得知,當發生雜湊衝突並且size大於閾值的時候,需要進行陣列擴容,擴容時,需要新建一個長度為之前陣列2倍的新的陣列,然後將當前的Entry陣列中的元素全部傳輸過去,擴容後的新陣列長度為之前的2倍,所以擴容相對來說是個耗資源的操作。

4、擴容操作

擴容操作通過resize操作實現:

 //按新的容量擴容Hash表  
    void resize(int newCapacity) {  
        Entry[] oldTable = table;//老的資料  
        int oldCapacity = oldTable.length;//獲取老的容量值  
        if (oldCapacity == MAXIMUM_CAPACITY) {//老的容量值已經到了最大容量值  
            threshold = Integer.MAX_VALUE;//修改擴容閥值  
            return;  
        }  
        //新的結構  
        Entry[] newTable = new Entry[newCapacity];  
        transfer(newTable, initHashSeedAsNeeded(newCapacity));//將老的表中的資料拷貝到新的結構中  
        table = newTable;//修改HashMap的底層陣列  
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改閥值  
    }  

如果陣列進行擴容,陣列長度發生變化,而儲存位置 index = h&(length-1),index也可能會發生變化,需要重新計算index,我們先來看看transfer這個方法:

//將老的表中的資料拷貝到新的結構中  
    void transfer(Entry[] newTable, boolean rehash) {  
        int newCapacity = newTable.length;//容量  
        for (Entry<K,V> e : table) { //遍歷所有桶
            while(null != e) {  //遍歷桶中所有元素(是一個連結串列)
                Entry<K,V> next = e.next;  
                if (rehash) {//如果是重新Hash,則需要重新計算hash值  
                    e.hash = null == e.key ? 0 : hash(e.key);  
                }  
                int i = indexFor(e.hash, newCapacity);//定位Hash桶  
                e.next = newTable[i];//元素連線到桶中,這裡相當於單鏈表的插入,總是插入在最前面
                newTable[i] = e;//newTable[i]的值總是最新插入的值
                e = next;//繼續下一個元素  
            }  
        }  
    }  

這個方法將老陣列中的資料逐個連結串列地遍歷,重新計算後放入新的擴容後的陣列中,我們的陣列索引位置的計算是通過 對key值的hashcode進行hash擾亂運算後,再通過和 length-1進行位運算得到最終陣列索引位置。
注意:HashMap陣列元素長度的設計
通過原始碼可以發現,hashMap的陣列長度一定保持2的次冪,這樣做有什麼好處呢?

//根據Hash值和Hash表的大小選擇合適的Hash桶  
    static int indexFor(int h, int length) {  
        return h & (length-1);  
    }  

如果length為2的次冪,其二進位制表示就是100….0000;則length-1 轉化為二進位制必定是0111….11的形式,在於h的二進位制與操作效率會非常的快,而且空間不浪費;如果length不是2的次冪,比如length為15,則length-1為14,對應的二進位制為1110,再於h與操作,
最後一位都為0,所以0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不會存放元素了,空間浪費相當大,更糟的是這種情況中,陣列可以使用的位置比陣列長度小了很多,這意味著進一步增加了碰撞的機率,減慢了查詢的效率!這樣就會造成空間的浪費。

5、get操作

    //獲取key值為key的元素值  
    public V get(Object key) {  
        if (key == null)//如果Key值為空,則獲取對應的值,這裡也可以看到,HashMap允許null的key,其內部針對null的key有特殊的邏輯  
            return getForNullKey();  
        Entry<K,V> entry = getEntry(key);//獲取實體  

        return null == entry ? null : entry.getValue();//判斷是否為空,不為空,則獲取對應的值  
    }  

    //獲取key為null的實體  
    private V getForNullKey() {  
        if (size == 0) {//如果元素個數為0,則直接返回null  
            return null;  
        }  
        //key為null的元素儲存在table的第0個位置  
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
            if (e.key == null)//判斷是否為null  
                return e.value;//返回其值  
        }  
        return null;  
    }  

get方法通過key值返回對應value,如果key為null,直接去table[0]處檢索。我們再看一下getEntry這個方法:

//獲取鍵值為key的元素  
    final Entry<K,V> getEntry(Object key) {  
        if (size == 0) {//元素個數為0  
            return null;//直接返回null  
        }  

        int hash = (key == null) ? 0 : hash(key);//獲取key的Hash值  
        for (Entry<K,V> e = table[indexFor(hash, table.length)];//根據key和表的長度,定位到Hash桶  
             e != null;  
             e = e.next) {//進行遍歷  
            Object k;  
            if (e.hash == hash &&  
                ((k = e.key) == key || (key != null && key.equals(k))))//判斷Hash值和對應的key,合適則返回值  
                return e;  
        }  
        return null;  
    }  

可以看出,get方法的實現相對簡單,key(hashcode)–>hash–>indexFor–>最終索引位置,找到對應位置table[i],再檢視是否有連結串列,遍歷連結串列,通過key的equals方法比對查詢對應的記錄。要注意的是,有人覺得上面在定位到陣列位置之後然後遍歷連結串列的時候,e.hash == hash這個判斷沒必要,僅通過equals判斷就可以。其實不然,試想一下,如果傳入的key物件重寫了equals方法卻沒有重寫hashCode,而恰巧此物件定位到這個陣列位置,如果僅僅用equals判斷可能是相等的,但其hashCode和當前物件不一致,這種情況,根據Object的hashCode的約定,不能返回當前物件,而應該返回null。
下面來舉一個例子看一下:

package com.kang.test;

import java.util.HashMap;

public class Test {

    private static class Person {
        int ID;
        String name;

        public Person(int idCard, String name) {
            this.ID = idCard;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Person person = (Person) o;
            // 兩個物件是否等值,通過ID來確定
            return this.ID == person.ID;
        }

    }

    public static void main(String[] args) {
        HashMap<Person, String> map = new HashMap<>();
        Person person = new Person(1234, "kang");
        map.put(person, "25歲");
        // get取出,從邏輯上講應該能輸出“25歲”
        System.out.println("結果:" + map.get(new Person(1234, "kang")));//注意這裡是new一個物件
    }
}

程式執行結果是:結果:null
為什麼會出現這樣的結果呢?我們只重寫了Person的equal方法,儘管我們在進行get和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的),但由於沒有重寫hashCode方法,而我們在進行get操作時使用的是map.get(new Person(1234, "kang"))),注意是new操作新建了一個物件。因為沒有重寫hashcode方法,所以兩者的hashcode是不同的。所以put操作時得到的hashcode1和get操作時得到的hashcode2是不同的。由於hashcode1不等於hashcode2,導致沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,但是也會判斷其entry的hash值是否相等,上面get方法中有提到。)。
解決方法很簡單,只需要重寫hashcode方法即可。如下:

        @Override
        public int hashCode() {
            return this.ID;//這裡為了簡單起見,直接將ID值作為hashcode的值
        }

    }

如此一來,輸出結果便是正確的。所以,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個物件,呼叫hashCode方法要返回同樣的整數值。而如果equals判斷不相等的兩個物件,其hashCode可以相同(只不過會發生雜湊衝突,應儘量避免)。

三、HashMap的使用總結

  • HashMap是基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久不變(發生擴容時,元素位置會重新分配)。
  • 迭代 collection 檢視所需的時間與 HashMap 例項的“容量”(桶的數量)及其大小(鍵-值對映關係數)成比例。所以,如果迭代效能很重要,則不要將初始容量設定得太高(或將載入因子設定得太低)。
  • HashMap 的例項有兩個引數影響其效能:初始容量 和載入因子。容量 是雜湊表中桶的數量,初始容量只是雜湊表在建立時的容量。載入因子 是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的桶數。通常,預設載入因子 (.75) 在時間和空間成本上尋求一種折衷。載入因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點)。在設定初始容量時應該考慮到對映中所需的條目數及其載入因子,以便最大限度地減少 rehash 操作次數。如果初始容量大於最大條目數除以載入因子,則不會發生 rehash 操作。
    如果很多對映關係要儲存在 HashMap 例項中,則相對於按需執行自動的 rehash 操作以增大表的容量來說,使用足夠大的初始容量建立它將使得對映關係能更有效地儲存。
  • HashMap的實現不是同步的。如果在多執行緒操作下,應該使用 Collections.synchronizedMap 方法來“包裝”該對映。最好在建立時完成這一操作,以防止對對映進行意外的非同步訪問,如下所示:
    Map m = Collections.synchronizedMap(new HashMap(…));
  • 由所有此類的“collection 檢視方法”所返回的迭代器都是快速失敗 的:在迭代器建立之後,如果從結構上對對映進行修改,除非通過迭代器本身的 remove 方法,其他任何時間任何方式的修改,迭代器都將丟擲 ConcurrentModificationException。

相關推薦

HashMap原始碼解析基於JDK1.7

一、HashMap簡介 雜湊表(hash table)也叫散列表,是一種非常重要的資料結構,應用場景及其豐富,許多快取技術(比如memcached)的核心其實就是在記憶體中維護一張大的雜湊表,而HashMap的實現原理就是基於此。那麼什麼是雜湊表呢? 在討論

HashMap原始碼解析基於JDK1.8

雜湊是一種用於以常數平均時間執行插入、刪除和查詢的技術。HashMap是基於雜湊表的Map介面實現,該實現提供了所有可選的對映操作,並允許使用 空值和空鍵。(HashMap 類大致等同於Hashtable,除了它是不同步的並且允許為空值。)這個類不能保證M

java集合之----HashMap原始碼分析基於JDK1.7與1.8

一、什麼是HashMap 百度百科這樣解釋: 簡而言之,HashMap儲存的是鍵值對(key和value),通過key對映到value,具有很快的訪問速度。HashMap是非執行緒安全的,也就是說在多執行緒併發環境下會出現問題(死迴圈) 二、內部實現 (1)結構 HashM

LinkedHashMap及其原始碼分析基於JDK1.7

LinkedHashMap及其原始碼分析 閱讀目錄 什麼是LinkedHashMap LinkedHashMap補充說明 LinkedHashMap的陣列結構 LinkedHashMap繼承的類與實現的介面 LinkedHashMap原始碼中雙向連結串列的

ArrayList實現原理以及原始碼解析補充JDK1.7,1.8

ArrayList實現原理以及原始碼解析(補充JDK1.7,1.8) ArrayList的基本知識在上一節已經討論過,這節主要看ArrayList在JDK1.6到1.8的一些實現變化。 JDK版本不一樣,ArrayList類的原始碼也不一樣。 1、ArrayList類結構:

ConcurrentHashMap原始碼刨析基於jdk1.7

看原始碼前我們必須先知道一下ConcurrentHashMap的基本結構。ConcurrentHashMap是採用分段鎖來進行併發控制的。 其中有一個內部類為Segment類用來表示鎖。而Segment類裡又有一個HashEntry<K,V>[]陣列,這個陣列才是真正用 來存放我們的key-v

java集合之----ArrayList原始碼分析基於jdk1.8

一、ArrayList 1、ArrayList是什麼: ArrayList就是動態陣列,用MSDN中的說法,就是Array的複雜版本,它提供了動態的增加和減少元素,實現了ICollection和IList介面,靈活的設定陣列的大小等好處,實現了Randomaccess介面,支援快速隨

JAVA常用集合框架原始碼解析基於1.8開題篇

倪升武的部落格中有一個小專題,讀完之後,發現博主的分析基本是基於JAVA1.7的,這裡我基於JAVA1.8給出一些新的解讀。但是對於JAVA1.8新增的一些新特性可能不太會作過多的分析(畢竟本人目前水平有限,且本部落格的寫作初衷也是以基礎學習為主),在徹底淺讀完

WeakHashMap原始碼探討基於JDK1.8

WeakHashMap簡介 WeakHashMap跟普通的HashMap不同,WeakHashMap的行為一定程度上基於垃圾收集器的行為,因此一些Map資料結構對應的常識在WeakHashMap上會失效——size()方法的返回值會隨著程式的執行變小,isE

HashSet原始碼探討基於JDK1.8

HashSet 簡介 如果對HashMap沒有了解,應該先學習HashMap再學習HashSet 。 HashSet結構 publicclassHashSet<E>extendsA

HashMap原始碼分析基於1.8

HashMap1.7和1.8變動比較多。 關於HashMap 1.7的版本,倪升武的部落格總結的很好。 這裡我主要來介紹一下1.8中的HashMap。由於HashMap原始碼太長,我只挑選了部分進行分析,如果有沒有分析到的重點難點或者大家有疑問的地方,希望大

ConcurrentHashMap & HashMap最清晰的底層原理分析基於JDK1.7跟1.8比較

前言 Map 這樣的 Key  Value 在軟體開發中是非常經典的結構,常用於在記憶體中存放資料。 本篇主要想討論 ConcurrentHashMap 這樣一個併發容器,在正式開始之前我覺得有必要談談 HashMap,沒有它就不會有後面的 ConcurrentHashM

JDK1.8 HashMap原始碼解析不分析紅黑樹部分

一、HashMap資料結構        HashMap由 陣列+連結串列+紅黑樹實現,桶中元素可能為連結串列,也可能為紅黑樹。為了提高綜合(查詢、新增、修改)效率,當桶中元素數量超過TREEIFY_THRESHOLD(預設為8)時,連結串列儲存改為紅黑樹儲存,當桶中元素數量

JAVA集合原始碼解析 Hashtable探索基於JDK1.8

JDK1.8Hashtable探索 本文的討論分析是基於JDK1.8進行的 依舊是採用前幾篇文章的大綱來進行介紹 1.簡介 Hashtable 採用陣列+單鏈表來實現的,Hashtable 實現了一個雜湊表,它將鍵對映到值。任何非 nu

Jdk1.8集合框架之HashMap原始碼解析詳細解析紅黑樹

HashMap特點 不同步,支援null的鍵和值,put或get操作通常是常數時間。 Map介面的實現。 去掉了Hashtable的contains(Object value)方法,保留containsKey和containsValue方法。 使用

HashMap原始碼探究死鎖/擴容JDK1.7】【JDK1.8】

先說HashMap最重要的一點:缺點        HashMap的缺點我們大都聽說過,其在高併發的情況下表現較差,會出現一些奇奇怪怪的問題,比如使CPU使用率提高到100%(此處打個小差,因為前幾天,我的伺服器莫名其妙CPU佔用率也達到了100%,我還以為是跑了哪個專案寫

基於jdk1.8的HashMap原始碼分析溫故學習

鼎力推薦一下一. HashMap結構      HashMap在jdk1.6版本採用陣列+連結串列的儲存方式,但是到1.8版本時採用了陣列+連結串列/紅黑樹的方式進行儲存,有效的提高了查詢時間,解決衝突。這裡有一篇部落格寫的非常好,HashMap的結構圖也畫的非常清楚,鼎力推

1.走進java本次閱讀基於jdk1.7

jvm 深入理解java虛擬機 本次閱讀基於jdk1.71.1.java虛擬機類別和發展史:Sun Classic / Exact VM:世界上第一款商用Java虛擬機,Sun公司發布JDK 1.0,Java語言首次擁有了商用的正式運行環境,這個JDK中所帶的虛擬機就是Classic VMSun Ho

HashMap原始碼解析JDK8

前言 這段時間有空,專門填補了下基礎,把常用的ArrayList、LinkedList、HashMap、LinkedHashMap、LruCache原始碼看了一遍,List相對比較簡單就不單獨介紹了,Map準備用兩篇的篇幅,分別介紹HashMap和(LruCache+LinkedHa

一篇文章徹底讀懂HashMapHashMap原始碼解析

就身邊同學的經歷來看,HashMap是求職面試中名副其實的“明星”,基本上每一加公司的面試多多少少都有問到HashMap的底層實現原理、原始碼等相關問題。 在秋招面試準備過程中,博主閱讀過很多關於HashMap原始碼分析的文章,漫長的拼湊式閱讀之後,博主沒有看到過