HashMap爲何會是面試中的常客呢?我以爲有如下幾點緣由:
* 考察你閱讀源碼的能力
* 是否瞭解內部數據結構
* 是否瞭解其存儲和查詢邏輯
* 對非線程安全狀況下的使用考慮
前段時間一同事面試螞蟻金服,就被問到了這個問題;其實不少狀況下都是從hashMap,hashTable,ConcurrentHahMap三者之間的關係衍生而出,固然也有直接就針對hashMap原理直接進行考察的。實際上本質都同樣,就是爲了考察你是否對集合中這些經常使用集合的原理、實現和使用場景是否清楚。一方面是咱們開發中用的多,固然用的人也就多,可是用的好的人卻很少(我也用的多,用的也很差)。因此就藉此機會(強行蹭一波)再來捋一捋這個HashMap。 本文基於jdk1.7.0_80;jdk 1.8以後略有改動,這個後面細說。html
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
複製代碼
hashMap實現了Map、Cloneable、Serializable三個接口,而且繼承了AbstractMap這個抽象類。hashTable繼承的是Dictionary這個類,同時也實現了Map、Cloneable、Serializable三個接口。面試
/**
* The default initial capacity - MUST be a power of two.
* 默認初始容量-必須是2的冪。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
複製代碼
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*若是有一個更大的值被用於構造HashMap,則使用最大值
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
複製代碼
/**
* The load factor used when none specified in constructor.
* 加載因子,若是構造函數中沒有指定,則使用默認的
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
複製代碼
/**
* An empty table instance to share when the table is not inflated.
* 當表不膨脹時共享的空表實例。
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
複製代碼
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
複製代碼
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
複製代碼
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold;
複製代碼
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
複製代碼
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
複製代碼
/**
* A randomizing value associated with this instance that is applied to
* hash code of keys to make hash collisions harder to find. If 0 then
* alternative hashing is disabled.
*/
transient int hashSeed = 0;
複製代碼
static class Entry<K,V> implements Map.Entry<K,V>
複製代碼
hashmap中是經過使用一個繼承自Map中內部類Entry的Entry靜態內部類來存儲每個K-V值的。看下具體代碼:數組
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //鍵對象
V value; //值對象
Entry<K,V> next; //指向鏈表中下一個Entry對象,可爲null,表示當前Entry對象在鏈表尾部
int hash; //鍵對象的hash值
/**
* 構造對象
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
/**
* 獲取key
*/
public final K getKey() {
return key;
}
/**
* 獲取value
*/
public final V getValue() {
return value;
}
/**
* 設置value,這裏返回的是oldValue(這個不太明白,哪位大佬清楚的能夠留言解釋下,很是感謝)
*/
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
/**
* 重寫equals方法
*/
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
/**
* 重寫hashCode方法
*/
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { } } 複製代碼
HashMap是一個用於存儲Key-Value鍵值對的集合,每個鍵值對也叫作Entry。這些個鍵值對(Entry)分散存儲在一個數組當中,這個數組就是HashMap的主幹(也就是上面的table--桶)。 看一張圖:安全
public static void main(String[] args) throws Exception {
HashMap<String, Object> map=new HashMap<>();
for (int i = 0; i < 170; i++) {
map.put("key"+i, i);
}
System.out.println(map);
}
複製代碼
咱們平時在開發是最經常使用的hashMap中的方法無非就是先建立一個HashMap對象,而後存,接着取;對應的方法就是:bash
構造函數數據結構
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity 指定的初始化容量大小
* @param loadFactor the load factor 指定的負載因子
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
//若是初始化容量小於0,則拋出異常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//若是初始化容量大於最大容量,則使用默認最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//若是負載因子小於0或者非數值類型,則拋出異常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//初始化負載因子
this.loadFactor = loadFactor;
//初始化threshold
threshold = initialCapacity;
//這個初始化方法是個空方法,應該是意在HashMap的子類中由使用者自行重寫該方法的具體實現
init();
}
複製代碼
另外兩個構造方法實際上都是對上面這個構造方法的調用:併發
//只制定默認容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用HashMap默認的容量大小和負載因子
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
複製代碼
還有一個是:app
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);
putAllForCreate(m);
}
複製代碼
構造一個映射關係與指定 Map 相同的新 HashMap。所建立的 HashMap 具備默認加載因子 (0.75) 和足以容納指定 Map 中映射關係的初始容量。dom
put方法
首先,咱們都知道hashmap中的key是容許爲null的,這一點也是面試中最常問到的點。那我先看下爲何能夠存null做爲key值。函數
public V put(K key, V value) {
//若是table是空的
if (table == EMPTY_TABLE) {
//inflate:擴容/膨脹的意思
inflateTable(threshold);
}
//若是key爲null 此處敲下桌子,爲何能夠存null?
if (key == null)
//執行putForNullKey方法,這個方法的做用是若是key爲null,就將當前的k-v存放到table[0],即第一個桶。
return putForNullKey(value);
//對key進行一次hash運算,獲取hash值
int hash = hash(key);
//根據key值得hash值和表的長度來計算索引位置
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))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
//上面Entry中的setValue中也有提到,返回的都是舊的數據
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
複製代碼
hash方法: 檢索對象哈希代碼,並將附加哈希函數應用於結果哈希,該哈希函數防止質量差的哈希函數。 這是相當重要的,由於HashMap使用兩個長度的哈希表,不然會碰到hashCode的衝突,這些hashCodes在低位上沒有區別。 注意:空鍵老是映射到散列0,所以索引爲0。
/**
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
//這個函數確保在每一個比特位置上僅以恆定倍數不一樣
//的散列碼具備有限數量的衝突(在默認加載因子下大約爲8)。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
複製代碼
衝突具體過程描述:
public V get(Object key) {
//和存null key同樣,取的時候也是從table[0]取
if (key == null)
return getForNullKey();
//獲取entry
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
複製代碼
getEntry方法
final Entry<K,V> getEntry(Object key) {
//size等於0,說明當前hashMap中沒有元素,直接返回null(每一個entry默認值爲null)
if (size == 0) {
return null;
}
//根據key值計算hash值
int hash = (key == null) ? 0 : hash(key);
//經過hash值獲取到索引位置,找到對應的桶鏈進行遍歷查找
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;
}
複製代碼
在前面提到過threshold,擴容變量,表示當HashMap的size大於threshold時會執行resize操做。其計算方式是:threshold=capacity*loadFactor。 從上面的式子中咱們能夠得知hashmap的擴容時機是當前當前size的值超過容量乘以負載因子時就會觸發擴容。來看下源碼:
void addEntry(int hash, K key, V value, int bucketIndex) {
//若是當前size超過threshold 而且知足桶索引位置不爲null的狀況下,擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
//擴容以後爲原來的兩倍
resize(2 * table.length);
//從新計算hash值
hash = (null != key) ? hash(key) : 0;
//重寫計算索引
bucketIndex = indexFor(hash, table.length);
}
//執行具體的插入操做
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//先取到當前桶的entry
Entry<K,V> e = table[bucketIndex];
//將新的數據插入到table[bucketIndex],再將以前的entry經過鏈表簡介到table[bucketIndex]的next指向;前面的圖已經進行了描述。
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
複製代碼
須要注意的是,擴容並非在hashmap滿了以後才進行的,看下面斷點:
本文就單純的扒了一波源碼,並對源碼中的註釋並結合本身的理解進行了翻譯,經過斷點調試簡單的介紹了尾插法在hashmap的應用。最後經過幾張圖描述了下hashmap發生索引衝突時的解決方案。hashmap在面試時真的是可深可淺,可是源碼的閱讀仍是頗有必要的,下面推薦兩篇博客給你們。