HashMap總結


  最近朋友推薦的一個很好的工作,又是面了2輪沒通過,已經是好幾次朋友內推沒過了,覺得挺對不住朋友的。面試反饋有一方面是有些方面理解思考的還不夠,平時也是項目進度比較緊,有些方面趕進度時沒有理解清楚的后面接着做新需求沒時間或者給忘了。以后還是得抽時間深入理解學習一些知識了,后面重點是知識深度,多思考。

  今天把面試問的較多的HashMap源碼看了下,相關知識做了個總結,希望對大家有幫助。如果寫的有問題的地方,歡迎討論。

  轉載請注明出處:http://www.cnblogs.com/John-Chen/p/4375046.html 

  

基本結構

鏈表結構:

static class HashMapEntry<K, V> implements Entry<K, V> {

        final K key;

        V value;

        final int hash;

        HashMapEntry<K, V> next;

        ......

}

 

數組存儲所有鏈表:

transient HashMapEntry<K, V>[] table;   //后面tab=table

 

key的hash值的計算:

int hash = Collections.secondaryHash(key);

 

table中HashMapEntry位置的計算:

//通過key的hash值獲得,因為HashMap數組的大小總是2^n,所以實際的運算就是 (0xfff...ff) & hash ,這里的tab.length-1相當於一個mask,濾掉了大於當前長度位的hash,使每個i都能插入到數組中。

int index = hash & (tab.length - 1); 

 

新增元素:

//放在鏈表的最前面,next = table[index]

table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);

 

取元素:

//找到key的hash對應的HashMapEntry,然后遍歷鏈表,通過key.equals取值

for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)]; e != null; e = e.next) {

      K eKey = e.key;

      if (eKey == key || (e.hash == hash && key.equals(eKey))) {

           return e.value;

      }

}

 

 

常見問題

 (其實了解上面的基本知識,下面的很多問題都好理解了)

 

當兩個不同的鍵對象的hashcode相同時會發生什么? 

它們會儲存在同一個bucket位置的HashMapEntry組成的鏈表中。

 

 

如果兩個鍵的hashcode相同,你如何獲取值對象?

當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,找到bucket位置之后,會調用keys.equals()方法去找到鏈表中正確的節點。

  

什么是hash,什么是碰撞,什么是equals ?

Hash:是一種信息摘要算法,它還叫做哈希,或者散列。我們平時使用的MD5,SHA1都屬於Hash算法,通過輸入key進行Hash計算,就可以獲取key的HashCode(),比如我們通過校驗MD5來驗證文件的完整性。

對於HashCode,它是一個本地方法,實質就是地址取樣運算

 

碰撞:好的Hash算法可以出計算幾乎出獨一無二的HashCode,如果出現了重復的hashCode,就稱作碰撞;

就算是MD5這樣優秀的算法也會發生碰撞,即兩個不同的key也有可能生成相同的MD5。

 

HashCode,它是一個本地方法,實質就是地址取樣運算;

==是用於比較指針是否在同一個地址;

equals與==是相同的。

  

如何減少碰撞?

使用不可變的、聲明作final的對象,並且采用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇

 

如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦?

默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因為它調用hash方法找到新的bucket位置。

/**

     * The default load factor. Note that this implementation ignores the

     * load factor, but cannot do away with it entirely because it's

     * mentioned in the API.

     *

     * <p>Note that this constant has no impact on the behavior of the program,

     * but it is emitted as part of the serialized form. The load factor of

     * .75 is hardwired into the program, which uses cheap shifts in place of

     * expensive division.

     */

    static final float DEFAULT_LOAD_FACTOR = .75F;

 

重新調整HashMap大小存在什么問題嗎?

(當多線程的情況下,可能產生條件競爭(race condition))

當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那么就死循環了。這個時候,你可以質問面試官,為什么這么奇怪,要在多線程的環境下使用HashMap呢?

 

為什么String, Interger這樣的wrapper類適合作為鍵?

因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那么就不能從HashMap中找到你想要的對象。不可變性還有其他的優點如線程安全。如果你可以僅僅通過將某個field聲明成final就能保證hashCode是不變的,那么請這么做吧。因為獲取對象的時候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個方法是非常重要的。如果兩個不相等的對象返回不同的hashcode的話,那么碰撞的幾率就會小些,這樣就能提高HashMap的性能。

  

可以使用自定義的對象作為鍵嗎?

當然你可能使用任何對象作為鍵,只要它遵守了equals()和hashCode()方法的定義規則,並且當對象插入到Map中之后將不會再改變了。如果這個自定義對象時不可變的,那么它已經滿足了作為鍵的條件,因為當它創建之后就已經不能改變了。

 

可以使用CocurrentHashMap來代替Hashtable嗎?

Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因為它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的線程安全性。

 

能否讓HashMap同步?

HashMap可以通過下面的語句進行同步:

Map m = Collections.synchronizeMap(hashMap);

 

HashMap和Hashtable的區別

主要的不同:線程安全以及速度。僅在你需要完全的線程安全的時候使用Hashtable,而如果你使用Java 5或以上的話,請使用ConcurrentHashMap吧。

 HashMap和Hashtable都實現了Map接口,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

 HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。

HashMap是非synchronized,而Hashtable是synchronized,這意味着Hashtable是線程安全的,多個線程可以共享一個Hashtable;而如果沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。

另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它線程改變了HashMap的結構(增加或者移除元素),將會拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別。

由於Hashtable是線程安全的也是synchronized,所以在單線程環境下它比HashMap要慢。如果你不需要同步,只需要單一線程,那么使用HashMap性能要好過Hashtable。

HashMap不能保證隨着時間的推移Map中的元素次序是不變的。

要注意的一些重要術語:

1) sychronized意味着在一次僅有一個線程能夠更改Hashtable。就是說任何線程要更新Hashtable時要首先獲得同步鎖,其它線程要等到同步鎖被釋放之后才能再次獲得同步鎖更新Hashtable。

2) Fail-safe和iterator迭代器相關。如果某個集合對象創建了Iterator或者ListIterator,然后其它的線程試圖“結構上”更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程可以通過set()方法更改集合對象是允許的,因為這並沒有從“結構上”更改集合。但是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。

3) 結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。

  

HashMap和HashSet的區別

什么是HashSet

HashSet實現了Set接口,它不允許集合中有重復的值,當我們提到HashSet時,第一件事情就是在將對象存儲在HashSet之前,要先確保對象重寫equals()和hashCode()方法,這樣才能比較對象的值是否相等,以確保set中沒有儲存相等的對象。如果我們沒有重寫這兩個方法,將會使用這個方法的默認實現。

 

public boolean add(Object o)方法用來在Set中添加元素,當元素值重復時則會立即返回false,如果成功添加的話會返回true。

 

什么是HashMap

HashMap實現了Map接口,Map接口對鍵值對進行映射。Map中不允許重復的鍵。Map接口有兩個基本的實現,HashMap和TreeMap。TreeMap保存了對象的排列次序,而HashMap則不能。HashMap允許鍵和值為null。HashMap是非synchronized的,但collection框架提供方法能保證HashMap synchronized,這樣多個線程同時訪問HashMap時,能保證只有一個線程更改Map。

public Object put(Object Key,Object value)方法用來將元素添加到map中。

 

HashSet和HashMap的區別

HashMap實現了Map接口 HashSet實現了Set接口

HashMap儲存鍵值對 HashSet僅僅存儲對象

使用put()方法將元素放入map中 使用add()方法將元素放入set中

HashMap中使用鍵對象來計算hashcode值 HashSet使用成員對象來計算hashcode值,對於兩個對象來說hashcode可能相同,所以equals()方法用來判斷對象的相等性,如果兩個對象不同的話,那么返回false

HashMap比較快,因為是使用唯一的鍵來獲取對象 HashSet較HashMap來說比較慢

  

HashMap的復雜度

HashMap整體上性能都非常不錯,但是不穩定,為O(N/Buckets),N就是以數組中沒有發生碰撞的元素。

 

                    獲取            查找         添加/刪除     空間

ArrayList     O(1)             O(1)             O(N)         O(N)

LinkedList   O(N)             O(N)             O(1)         O(N)

HashMap   O(N/Bucket_size)   O(N/Bucket_size)   O(N/Bucket_size) O(N)

注:發生碰撞實際上是非常稀少的,所以N/Bucket_size約等於1

 

對key進行Hash計算

在JDK8中,由於使用了紅黑樹來處理大的鏈表開銷,所以hash這邊可以更加省力了,只用計算hashCode並移動到低位就可以了

static final int hash(Object key) {

    int h;

    //計算hashCode,並無符號移動到低位

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

 

幾個常用的哈希碼的算法

1:Object類的hashCode.返回對象的內存地址經過處理后的結構,由於每個對象的內存地址都不一樣,所以哈希碼也不一樣。

/**

     * Returns an integer hash code for this object. By contract, any two

     * objects for which {@link #equals} returns {@code true} must return

     * the same hash code value. This means that subclasses of {@code Object}

     * usually override both methods or neither method.

     *

     * <p>Note that hash values must not change over time unless information used in equals

     * comparisons also changes.

     *

     * <p>See <a href="{@docRoot}reference/java/lang/Object.html#writing_hashCode">Writing a correct

     * {@code hashCode} method</a>

     * if you intend implementing your own {@code hashCode} method.

     *

     * @return this object's hash code.

     * @see #equals

     */

    public int hashCode() {

        int lockWord = shadow$_monitor_;

        final int lockWordMask = 0xC0000000;  // Top 2 bits.

        final int lockWordStateHash = 0x80000000;  // Top 2 bits are value 2 (kStateHash).

        if ((lockWord & lockWordMask) == lockWordStateHash) {

            return lockWord & ~lockWordMask;

        }

        return System.identityHashCode(this);

    }

 

2:String類的hashCode.根據String類包含的字符串的內容,根據一種特殊算法返回哈希碼,只要字符串所在的堆空間相同,返回的哈希碼也相同。

@Override public int hashCode() {

        int hash = hashCode;

        if (hash == 0) {

            if (count == 0) {

                return 0;

            }

            final int end = count + offset;

            final char[] chars = value;

            for (int i = offset; i < end; ++i) {

                hash = 31*hash + chars[i];

            }

            hashCode = hash;

        }

        return hash;

    }

 

3:Integer類,返回的哈希碼就是Integer對象里所包含的那個整數的數值,例如Integer i1=new Integer(100),i1.hashCode的值就是100 。由此可見,2個一樣大小的Integer對象,返回的哈希碼也一樣。

public int hashCode() {

        return value;

}

 

int,char這樣的基礎類,它們不需要hashCode.

 

 

插入包裝類到數組

(1). 如果輸入當前的位置是空的,就插進去,如圖,左為插入前,右為插入后

 

0           0

|           |

1 -> null   1 - > null

|           |

2 -> null   2 - > null

|           | 

..-> null   ..- > null

|           | 

i -> null   i - > new node

|           |

n -> null   n - > null

(2). 如果當前位置已經有了node,且它們發生了碰撞,則新的放到前面,舊的放到后面,這叫做鏈地址法處理沖突。

 

0           0

|           |

1 -> null   1 - > null

|           |

2 -> null   2 - > null

|           | 

..-> null   ..- > null

|           | 

i -> old    i - > new - > old

|           |

n -> null   n - > null

我們可以發現,失敗的hashCode算法會導致HashMap的性能下降為鏈表,所以想要避免發生碰撞,就要提高hashCode結果的均勻性。當然,在JDK8中,采用了紅黑二叉樹進行了處理,這個我們后面詳細介紹。

 

 

 

什么是Hash攻擊?

 通過請求大量key不同,但是hashCode相同的數據,讓HashMap不斷發生碰撞,硬生生的變成了SingleLinkedList

 0

|

1 -> a ->b -> c -> d(撞!撞!撞!復雜度由O(1)變成了O(N))

|

2 -> null(本應該均勻分布,這里卻是空的)

|

3 -> null

|

4 -> null

這樣put/get性能就從O(1)變成了O(N),CPU負載呈直線上升,形成了放大版DDOS的效果,這種方式就叫做hash攻擊。在Java8中通過使用TreeMap,提升了處理性能,可以一定程度的防御Hash攻擊。

 

擴容

如果當表中的75%已經被占用,即視為需要擴容了

初始容量:

if (capacity < MINIMUM_CAPACITY) {

            capacity = MINIMUM_CAPACITY;

        } else if (capacity > MAXIMUM_CAPACITY) {

            capacity = MAXIMUM_CAPACITY;

        } else {

            capacity = Collections.roundUpToPowerOfTwo(capacity);

        }
public static int roundUpToPowerOfTwo(int i) {

        i--; // If input is a power of two, shift its high-order bit right.

 

        // "Smear" the high-order bit all the way to the right.

        i |= i >>>  1;

        i |= i >>>  2;

        i |= i >>>  4;

        i |= i >>>  8;

        i |= i >>> 16;

 

        return i + 1;

    }

 

(threshold = capacity * load factor ) < size

它主要有兩個步驟:

 

1. 容量加倍

左移N位,就是2^n,用位運算取代了乘法運算

newCap = oldCap << 1;

newThr = oldThr << 1;

 

2. 遍歷計算Hash

for (int j = 0; j < oldCap; ++j) {

                Node<K,V> e;

                //如果發現當前有Bucket

                if ((e = oldTab[j]) != null) {

                    oldTab[j] = null;

                    //如果這里沒有碰撞

                    if (e.next == null)

                        //重新計算Hash,分配位置

                        newTab[e.hash & (newCap - 1)] = e;

                    //這個見下面的新特性介紹,如果是樹,就填入樹

                    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) {

                                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;

                        }

                    }

                }

            }

 

由此可以看出擴容需要遍歷並重新賦值,成本非常高,所以選擇一個好的初始容量非常重要。



 

如何提升性能?

解決擴容損失:如果知道大致需要的容量,把初始容量設置好以解決擴容損失;

比如我現在有1000個數據,需要 1000/0.75 = 1333 ,又 1024 < 1333 < 2048,所以最好使用2048作為初始容量。

2048=Collections.roundUpToPowerOfTwo(1333)

 

解決碰撞損失:使用高效的HashCode與loadFactor,這個...由於JDK8的高性能出現,這兒問題也不大了。

 

解決數據結構選擇的錯誤:在大型的數據與搜索中考慮使用別的結構比如TreeMap,這個就是積累了;

 

 

JDK8中HashMap的新特性

如果某個桶中的鏈表記錄過大的話(當前是TREEIFY_THRESHOLD = 8),就會把這個鏈動態變成紅黑二叉樹,使查詢最差復雜度由O(N)變成了O(logN)。

 

//e 為臨時變量,p為當前的鏈

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;

}

 

 

在Android中使用SparseArray代替HashMap

官方推薦使用SparseArray([spɑ:s] [ə'reɪ],稀疏的數組)或者LongSparseArray代替HashMap,目前國內好像涉及的比較少,容我先粘貼一段

 

Note that this container keeps its mappings in an array data structure, using a binary search to find keys. The implementation is not intended to be appropriate for data structures that may contain large numbers of items. It is generally slower than a traditional HashMap, since lookups require a binary search and adds and removes require inserting and deleting entries in the array.

 

For containers holding up to hundreds of items, the performance difference is not significant, less than 50%.

To help with performance, the container includes an optimization when removing keys: instead of compacting its array immediately, it leaves the removed entry marked as deleted. The entry can then be re-used for the same key, or compacted later in a single garbage collection step of all removed entries. This garbage collection will need to be performed at any time the array needs to be grown or the the map size or entry values are retrieved.

 

總的來說就是:

SparseArray使用基本類型(Primitive)中的int作為Key,不需要Pair<K,V>或者Entry<K,V>這樣的包裝類,節約了內存;

SpareAraay維護的是一個排序好的數組,使用二分查找數據,即O(log(N)),每次插入數據都要進行排序,同樣耗時O(N);而HashMap使用hashCode來加入/查找/刪除數據,即O(N/buckets_size);

總的來說,就是SparseArray針對Android嵌入式設備進行了優化,犧牲了微小的時間性能,換取了更大的內存優化;同時它還有別的優化,比如對刪除操作做了優化;

如果你的數據非常少(實際上也是如此),那么使用SpareArray也是不錯的;

 

 

重要結構及方法

static class HashMapEntry<K, V> implements Entry<K, V> {

        final K key;

        V value;

        final int hash;

        HashMapEntry<K, V> next;   //鏈表

        ......

}

 

/**

     * The table is rehashed when its size exceeds this threshold.

     * The value of this field is generally .75 * capacity, except when

     * the capacity is zero, as described in the EMPTY_TABLE declaration

     * above.

     */

    private transient int threshold;

 

/**

     * Maps the specified key to the specified value.

     *

     * @param key

     *            the key.

     * @param value

     *            the value.

     * @return the value of any previous mapping with the specified key or

     *         {@code null} if there was no such mapping.

     */

    public V put(K key, V value) {

        if (key == null) {

            return putValueForNullKey(value);

        }

 

        //二次hash,目的是為了讓所有的位數都來參加hash,防止沖突

        int hash = Collections.secondaryHash(key);   

        HashMapEntry<K, V>[] tab = table;

 

        //位置的獲得,tab.length-1與任何數相與都小於length

        int index = hash & (tab.length - 1); 

 

        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {

            //遍歷index處的鏈表

            if (e.hash == hash && key.equals(e.key)) {

                //替換並返回舊的value

                preModify(e);

                V oldValue = e.value;

                e.value = value;

                return oldValue;

            }

        }

 

        // No entry for (non-null) key is present; create one

        modCount++;

        if (size++ > threshold) {

            //檢查容量

            tab = doubleCapacity();

            index = hash & (tab.length - 1);

        }

        //添加新的value,放在鏈表的最前面,next = table[index]

        table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);

        return null;

    }

 

 

 

 

/**

     * Returns the value of the mapping with the specified key.

     *

     * @param key

     *            the key.

     * @return the value of the mapping with the specified key, or {@code null}

     *         if no mapping for the specified key is found.

     */

    public V get(Object key) {

        if (key == null) {

            HashMapEntry<K, V> e = entryForNullKey;

            return e == null ? null : e.value;

        }

 

        int hash = Collections.secondaryHash(key);

        HashMapEntry<K, V>[] tab = table;

        //找到hash對應的HashMapEntry,然后遍歷鏈表,通過key.equals取值

        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];

                e != null; e = e.next) {

            K eKey = e.key;

            if (eKey == key || (e.hash == hash && key.equals(eKey))) {

                return e.value;

            }

        }

        return null;

    }

 

 

/**

     * Doubles the capacity of the hash table. Existing entries are placed in

     * the correct bucket on the enlarged table. If the current capacity is,

     * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which

     * will be new unless we were already at MAXIMUM_CAPACITY.

     */

    private HashMapEntry<K, V>[] doubleCapacity() {

        //size++ > threshold時調用

        HashMapEntry<K, V>[] oldTable = table;

        int oldCapacity = oldTable.length;

        if (oldCapacity == MAXIMUM_CAPACITY) {

            return oldTable;

        }

        int newCapacity = oldCapacity * 2;

        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);

        if (size == 0) {

            return newTable;

        }

 

        for (int j = 0; j < oldCapacity; j++) {

            /*

             * Rehash the bucket using the minimum number of field writes.

             * This is the most subtle and delicate code in the class.

             */

            HashMapEntry<K, V> e = oldTable[j];

            if (e == null) {

                continue;

            }

            int highBit = e.hash & oldCapacity;

            HashMapEntry<K, V> broken = null;

            newTable[j | highBit] = e;

            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {

                int nextHighBit = n.hash & oldCapacity;

                if (nextHighBit != highBit) {

                    if (broken == null)

                        newTable[j | nextHighBit] = n;

                    else

                        broken.next = n;

                    broken = e;

                    highBit = nextHighBit;

                }

            }

            if (broken != null)

                broken.next = null;

        }

        return newTable;

    }


免責聲明!

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



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