面試必考之HashMap原始碼分析與實現
以下是JDK1.8之前版本的原始碼簡介
一、什麼是HashMap?
Hash:雜湊將一個任意的長度通過某種(hash函式演算法)演算法轉換成一個固定值。
Map:儲存的集合、類似於地圖X,Y座標的儲存
總結:通過hash出來的值,然後通過這個值定位到這個map,然後把這個value儲存到這個map中 ~~ hashMap基本原理
二、HashMap形式?
key,value。例如 wukong 30 、電話薄 a-z字母等等
二、面試中問到的問題?
1.0 key值可以為空嗎?
【回答】hashMap把Null當做一個key值來儲存,原因看原始碼
public V put (K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//原始碼中判斷了,如果key值未空的時候,沒有返回錯誤資訊,也是允許儲存的
if (key == null)
return putForNullKey(value);
int hash = hash(key);
.............
}
2.0 HashMap和Hashtable的區別
【回答】HashMap是Hashtable的輕量級實現(非執行緒安全的實現),他們都完成了Map介面,主要區別在於HashMap允許空(null)鍵值(key),由於非執行緒安全,效率上可能高於Hashtable。
HashMap允許將null作為一個entry的key或者value,而Hashtable不允許。
HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因為contains方法容易讓人引起誤解。
Hashtable繼承自Dictionary類,而HashMap是Java1.2引進的Map interface的一個實現。
最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多個執行緒訪問Hashtable時,不需要自己為它的方法實現同步,而HashMap 就必須為之提供外同步。
Hashtable和HashMap採用的hash/rehash演算法都大概一樣,所以效能不會有很大的差異。
3.0 如果Hash key重複了,那麼value值會覆蓋嗎?
【回答】不會覆蓋、簡單解釋:在Entry類中,有個Entry< K,V > next 例項變數;它是來儲存hashKey衝突時,存放就的value值。不會覆蓋。詳細也是見原始碼
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
......
}
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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))) {
//這裡實際上是先獲取原來的value,儲存老的備份,可以通過 xxx.get("xiaozheng").next.getValue()獲取。
V oldValue = e.value; // 假設原來的是30,傳進來的是31 。 目前還是30
//然後在把當前的value賦給它
e.value = value; // 31
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
4.0 HashMap什麼時候做擴容?
【回答】在put的時候,HashMap集合的容量高於0.75的時候,進行擴容。而且擴容是偶數的,以雙倍的形式向上擴容。具體也是看原始碼
5.0 HashMap效能受什麼影響?
【回答】HashMap主要是受 初始化容量跟載入因子。初始化容量:建立一個HashMap預設給與多大的容量的值。載入因子:HashMap在擴容之前最大可以達到的容量。具體原因的話,看下面的“|不足之處“就明白了
6.0 HashMap table的資料結構?
【回答】陣列+連結串列。取兩者的優點
四、原始碼分析
4.1 初始化引數介紹
4.1.1 初始化容量
/**
* 初始化容量 1左移4位 6位
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
4.1.2 最大容量
/**
* 最大容量 1左移30位 也就是 2的30次方。儲存的HashMap的個數不能超過該最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
4.1.3 載入因子:這也就可以解釋為什麼上述我說:HashMap集合的容量高於0.75的時候,進行擴容。0.75不是我說的,而是在程式碼中定義好,0.75
/*
* 載入因子係數
* 可以理解成:在當前容量的四分之三的時候進行擴容。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.1.2 其他初始化引數
/*
* 建立一個Entry物件
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
/**
* 對table進行賦值
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size;
//擴容變數
int threshold;
//臨時載入因子
final float loadFactor;
//修改標記
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
4.2 HashMap構造方法
構造方法的話,我們只需要瞭解下述三個構造方法即可 - - 我們可以手動指定HashMap的初始化容量以及它的載入因子。這也是提高HashMap效能的一種方式。例如:如果你知道你的hashMap需要儲存10萬個map。那麼一開始可以調大你的初始化容量。避免一開始16個集合,多次擴容,多次拷貝帶來的時間、效能消耗
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();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
4.3 put方法分析
上述第一個問題跟第三個問題的答案就這下述程式碼裡面
public V put(K key, V value) {
//判斷table是否已經初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key未空的話,呼叫存空key的方法,沒有報錯,也就是支援空key的情況
if (key == null)
return putForNullKey(value);
//這也是hash的定義,雜湊,將key進行某種演算法(hash演算法),轉化成一個固定的值。也就是map資料的下標
int hash = hash(key);
int i = indexFor(hash, table.length);
//for迴圈判斷key的hash值是否是相等的,如果相等的話,就進行換位~~ 這裡結合下面下面的圖可以理解
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))) {
//這裡獲取原來舊的value
V oldValue = e.value;
//再把當前的value跟key對應,沒有覆蓋
e.value = value;
//儲存舊value
e.recordAccess(this);
return oldValue;
}
}
//修改標記+1
modCount++;
addEntry(hash, key, value, i);
return null;
}
//核心程式碼,if(),也就是當前容量達到0.75,就會執行resize()方法,進行擴容。這也是上述4.0問題的答案。
//下面我們可以看一下resize的方法--擴容方法
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;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
//老的table資料
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//建立Entry物件陣列
Entry[] newTable = new Entry[newCapacity];
//這個方法是幹什麼用的呢? -- 回答:賦值 ,
//可見每次擴容的時候都必須把原來就的資料賦值到新的陣列過去,這就產生很大的開銷,這也就能夠解釋為什麼載入因子和初始量影響著HashMap效能這個問題
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
4.4 get方法分析
比較簡單,緊跟4.5一起看
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
4.5 entry物件介紹
Entry有next屬性變數。專門用來處理key值出現重複的情況用的,詳細解釋看下圖片
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
//預設情況之下,next=null,當出現一個key相同的情況之下,當前的value就會被替換,同時當前next-》會指向原來的value。看圖片
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
.......
}
4.6 賦值原始碼分析
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;
}
}
}
五、不足之處
5.1 HashMap獲取Map集合的時間複雜度?
【回答】:與key值是否重複有關係,一般情況下g(O)1。如果出現key值重複的話,那就另外計算。key值是否重複取決於我們的Hash演算法
總結:時間複雜度:你的hash演算法絕對了你的效率
5.2 從伸縮性的角度分析不足之處
【回答】每當hashMap擴容的時候需要重新去add entry物件,需要重新Hash。然後放入我們新的entry table數組裡面。
如果你們的工作中你知道你的hashMap需要存多少值,幾千或者幾萬的時候,最好就是指定它們的擴容大小,防止在put的時候進行再次擴容 多次擴容