Java面試必問之---HashMap


   本文有些長,貼的源碼較多,請各位看官自備花生瓜子啤酒飲料礦泉水小板凳,且聽我慢慢道來。

   Java面試都會問集合,集合必問HashMap,CurrentHashMap,后面的套路就肯定會問多線程、線程安全等等,今天就來學習下HashMap,不對,是補習下。

1、HasMap的屬性

  先看下HashMap的繼承體系,它繼承自抽象類AbstractMap,實現了Map、Cloneable、Serializable接口,還有較常用的子類LinkedHashMap也實現了Map接口。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{...
public abstract class AbstractMap<K,V> implements Map<K,V> {...
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{...

  再看看HashMap的成員變量和一些默認值:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認的初始化數組大小,16
static final int MAXIMUM_CAPACITY = 1 << 30; // HashMap的最大長度
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 負載因子的默認值
static final Entry<?,?>[] EMPTY_TABLE = {}; // Entry數組默認為空
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // Entry數組
transient int size; // map中key-value 鍵值對的數量
int threshold; // 閾值,即table.length 乘 loadFactor
final float loadFactor; //負載因子,默認值為 DEFAULT_LOAD_FACTOR = 0.75 
transient int modCount; // HashMap結構被修改的次數
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; // 閾值的默認值
HashMap.Holder.trasient int hashSeed; // 翻譯過來叫哈希種子,是一個隨機數,
                       //它能夠減小hashCode碰撞的幾率,默認為0,表示不能進行選擇性哈希(我也不知道是啥意思)

  所以我們用默認構造方法new 出來的HashMap(),長度默認為16,閾值為12,並且size達到threshold,就會resize為原來的2倍。

  再看下HashMap的一些重要的內部類:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
}

  Entry實現了Map的內部接口Entry,它有四個屬性,key、value、Entry、hash,是HashMap內數組每個位置上真正存放元素的數據結構。

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator();
    }
    public boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<K,V> e = (Map.Entry<K,V>) o;
        Entry<K,V> candidate = getEntry(e.getKey());
        return candidate != null && candidate.equals(e);
    }
    public boolean remove(Object o) {
        return removeMapping(o) != null;
    }
    public int size() {
        return size;
    }
    public void clear() {
        HashMap.this.clear();
    }
}

  EntrySet 繼承了AbstractSet,它內部有個迭代器iterator,可以獲取Entry對象,方法contains用來判斷所給的對象是否包含在當前EntrySet中。

2、put、get、resize方法源碼分析

  我們知道HashMap,在jdk1.8之前底層用數組+鏈表實現的,jdk1.8改成了數組+鏈表+紅黑樹實現,以避免長鏈表帶來的遍歷效率低問題。

  1)jdk1.7下的源碼

    1.1)put()方法

public V put(K key, V value) {        
  if (table == EMPTY_TABLE) { //(1)

  inflateTable(threshold); }
if (key == null) //(2) return putForNullKey(value); int hash = hash(key); //(3) int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //(4) V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); //(5) return null; }
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; //長度必須是2的非零冪 return h & (length-1); //table數組的下標計算:hashCode與(table數組長度減一)做與(&)運算 } &運算,即同是1才為1,否則為0 例如:h1=3 h2=20 length=16 h1: 0011   h2: 10100   length-1: 1111 h1(index): 0011 = 3   h2(index): 0100 = 4 這樣運算得出的index就是舍棄了hashCode一部分高位的hash的值

  (1)首先判斷數組若為空,則創建一個新的數組;

  (2)如果key為null,遍歷table數組,如果找出key=null的位置,將value覆蓋,並返回舊的value,否則調用addEntry()將它保存到table[0]位置;

  (3)若key!=null,則計算出hashCode,算出下標 index,遍歷table

  (4)若找到hashCode與當前key的hashCode相等,並且key值也相同,那就覆蓋value的值,並且放回oldValue;

  (5)若沒滿足(4)中的條件,則調用方法addEntry(...),下面仔細看下這個方法

    若indexFor計算出來的下標在數組中不為空並且size達到閾值,則擴容,然后在index位置創建一個Entry,將key-value放進去。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length); 
        hash = (null != key) ? hash(key) : 0; // null的hashCode為0
        bucketIndex = indexFor(hash, table.length); 
    }
    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.2)get() 方法

public V get(Object key) {
    if (key == null) //(1) return getForNullKey();
    Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); //(4)
}

private V getForNullKey() { //(2) if (size == 0) {
        return null;
    }
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

final Entry<K,V> getEntry(Object key) { //(3) if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : 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 != null && key.equals(k))))
            return e;
    }
    return null;
}

    get() 方法就比較簡單啦:

      (1) 如果key為null,則判斷HashMap中是否有值,若沒有直接返回null;

      (2) 若有就遍歷table數組,找到null對應的value並返回;

      (3) 若key不為null,則獲取Entry,也就是一個遍歷table數組命中的過程;

      (4) 最后獲取Entry的value,並返回。

     1.3)resize() 方法

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) { //(1)
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity]; //(2)
    transfer(newTable, initHashSeedAsNeeded(newCapacity)); //(3)
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

    (1)首先將當前對象的一些屬性保存起來,如果當前HashMap的容量達到最大值,那就無法擴容了,將閾值設置為Integer的最大值並結束方法;

    (2)否則創建新的Entry數組,長度為newCapacity,在addEntry()方法中,我們知道newCapacity = 2 * table.length;

    (3)然后調用transfer()方法,此方法的作用是將當前數組中的Entry轉移到新數組中;

      在存入key-value時會調用initHashSeedAsNeeded()方法判斷是否需要rehash,該方法的過程見注釋,好吧,我也不知道為什么這樣處理得出的結果就能                判斷是否需要rehash,后面就是根據rehash重新計算下標,並將key-value存入新的table中。

/**
 * Transfers all entries from current table to newTable.
 */
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) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
/**
 * Initialize the hashing mask value. We defer initialization until we really need it.
 */
final boolean initHashSeedAsNeeded(int capacity) { 
    boolean currentAltHashing = hashSeed != 0; // 當前哈希種子是否為0
    boolean useAltHashing = sun.misc.VM.isBooted() && 
        (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); // 虛擬機是否啟動,當前數組容量是否大於閾值
    boolean switching = currentAltHashing ^ useAltHashing; // 做異或運算
    if (switching) { 
        hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; // 重置哈希種子
    }
    return switching; // 返回異或運算的結果,作為是否rehash的標准
}

 

   2)jdk1.8下的源碼

    jdk1.8中將Entry改為Node節點來實現的,屬性都是一樣的。

    2.1)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) // 如果數組是null或者數組為空,就調用resize()進行初始化
        n = (tab = resize()).length; 
    if ((p = tab[i = (n - 1) & hash]) == null)  //(n-1)&hash 算出下表,這個和1.7是一樣的
        tab[i] = newNode(hash, key, value, null);  // 如果當前計算出來的位置為null,就新建一個節點
    else {
        Node<K,V> e; K k;
        if (p.hash == hash && ((k = p.key) == key || 
          (key != null && key.equals(k)))) // 若計算出來的位置上不為null,它和傳入的key相比,hashCode相等並且key也相等 e = p; // 那么將p賦給e else if (p instanceof TreeNode) // 如果p是樹類型 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 則按照紅黑樹的結構存入進去 else { for (int binCount = 0; ; ++binCount) { // 遍歷p,p是鏈表 if ((e = p.next) == null) { // 如果p的下一個節點是尾節點(尾節點.next=null) p.next = newNode(hash, key, value, null); // 在p的后面創建一個節點,存放key/value(尾插法,多線程並發不會形成循環鏈表) if (binCount >= TREEIFY_THRESHOLD - 1) // TREEIFY_THRESHOLD = 8,即當binCount達到7時轉換成紅黑樹數據結構,                       // 因為binCount是從0開始的,達到7時p鏈表上就有8個節點了,所以是鏈表上達到8個節點時會轉變成紅黑樹。 treeifyBin(tab, hash); // 這里先就不展開了,紅黑樹不會,有時間再研究 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; // 若上面兩個條件都不滿足,此時e = p.next,也就是將p的下一個節點賦給p,進入下一次循環 } } if (e != null) { // existing mapping for key,jdk這段注釋意思是存在key的映射,我的理解是傳入的key在p位置找到它自己的坑被別人占了 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) // 下面就是將value存入被占的位置,並將舊的value返回 e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 修改次數加一 if (++size > threshold) // 若已有的鍵值對數大於閾值,就擴容 resize(); afterNodeInsertion(evict); return null; }

  下面盜個圖,嘿嘿,那老哥畫的太好了,圖片來源:http://www.importnew.com/20386.html,我自己又畫了下,加深點印象。

    2.2)get()方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> 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) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

  get()方法也沒什么,就是根據key的hashCode算出下標,找到對應位置上key與參數key是否相等,hash是否相等,如果是樹就獲取樹的節點,如果是鏈表就遍歷直到找到為止,找不到就返回null。

    2.3)resize()方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; 
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap就是原數組的長度
    int oldThr = threshold; //原閾值
    int newCap, newThr = 0;
    if (oldCap > 0) { 
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold 擴容成兩倍
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {      // zero initial threshold signifies using defaults,這里表示初始化resize的另一個作用
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    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<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //創建新數組,容量為原數組的兩倍
    table = newTab; //將它指向table變量
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) { //遍歷原數組
            Node<K,V> e;
            if ((e = oldTab[j]) != null) { //將不為null的j位置的元素指向e節點
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e; //若e是尾節點,或者說e后面沒有節點了,就將e指向新數組的e.hash&(newCap-1)位置
                else if (e instanceof TreeNode)
                    ((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) { //e.hash&oldCap 的值要么是0要么是oldCap ###
                            if (loTail == null)
                                loHead = e; // 第一次進來,先確定頭節點,以后都走else,loHead指向e
                            else
                               loTail.next = e; // 第二次進來時loTail的next指向e(e=e.next),
                             // 注意此時loHead的地址和loTail還是一樣的,所以loHead也指向e,
                             // 也就是說e被掛在了loHead的后面(尾插法,不會形成循環鏈表),
                             // 以此類推,后面遍歷的e都會被掛在loHead的后面。
                            loTail = e; // loTail指向e,第一次進來時頭和尾在內存中的指向是一樣的都是e,
                        // 第二次進來時,loTail指向了e(e=e.next),這時和loHead.next指向的對象是一樣的,
                        // 所以下一次進來的時候loHead可以找到loTail.next,並將e掛在后面。
                        // 這段不明白的可以參考:https://blog.csdn.net/u013494765/article/details/77837338
} else { // 和if里面的原理是一樣的 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; // 將loHead節點存到新數組中原下標位置 } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; // 將hiHead節點存到新數組中 [原下標+原數組長度] 的位置 } } } } } return newTab; }

   這里爭對 ### 標注的右岸代碼詳細講下:

    為什么(e.hash&oldCap) == 0為true或false就能判斷存放的位置是newTab[原下標],還是newTab[原下標+原數組長度],而不用像jdk1.7那樣每次都要rehash?

 

3、jdk1.7多線程並發形成循環鏈表問題

4、並發訪問HashMap會出現哪些問題,如何解決呢

  經過上面分析,我們知道jdk1.8已經不會在多線程下出現循環鏈表問題了,那還會出現哪些問題呢?

  如:數據丟失、結果不一致......

解決方案:

  (1)HashTable

    用synchronized鎖住整個table,效率太低,不好。

  (2)Collections.SynchronizedMap() 

    它是對put等方法用synchronized加鎖的,效率一般是不如ConcurrentHashMap的,用的不多。

  (3)ConcurrentHashMap

    采用鎖分段,segment,每次對要操作的那部分數據加鎖,並且get()是不用加鎖的,這效率就高多了。具體實現原理,且聽下回分解。

 

最后:文中若有寫的不對或者不好的地方,請各位看官指出,謝謝。

 

參考資料:1、https://juejin.im/post/5b551e8df265da0f84562403

      2、http://www.importnew.com/20386.html

     3、https://blog.csdn.net/u013494765/article/details/77837338#comments


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM