筆記來源: 尚硅谷
一、Map接口及其多個實現類的對比
首先看下Map及其實現類的類圖關系:
Map:雙列數據,存儲key-value對的數據
實現類 | 含義 |
---|---|
HashMap | 一般來說作為Map的主要實現類, 線程不安全 , 效率高 , 可以存儲null的key和value ① JDK7及以前:數組+鏈表 ② JDK8:數組+鏈表+紅黑樹 |
LinkedHashMap | 保證在遍歷map元素時,可以按照添加的順序實現遍歷。在原有的HashMap基礎上,添加了一對指針,指向前一個和后一個元素。對於頻繁的遍歷操作,此類執行效率高於HashMap |
Hashtable | 古老的實現類【甚至命名都不規范】,現在已經不怎么用了, 線程安全,效率低,不能存儲null的key和value |
TreeMap | 存儲有序的鍵值對,實現排序遍歷【按照key排】,底層使用紅黑樹 |
Properties | Hashtable子類,常用來處理配置文件,key和value都是string類型 |
常見面試題:
-
HashMap的底層實現原理
-
HashMap 和 Hashtable異同
-
ConcurrentHashMap 與 Hashtable區別
二、Map中存儲的key-value特點
-
Map中的key是無序的,不可重復的 --> key所在的類要重寫equals()和hashCode()方法【以HashMap為例】
-
Map中的value是無序的,可重復的 --> value所在類要重寫equals()fangfa1
一個鍵值對:key-value構成了一個Entry對象,是無序不可重復的,用set存放
三、HashMap在JDK7中的底層原理
HashMap map = new HashMap();
在實例化以后,底層創建了長度是16的一維數組 Entry[] table.
map.put(key1,value1)
調用key1 所在類的hashCode()計算key1哈希值,此哈希值經過某種算法計算以后,得到在Entry數組中的存放位置。
① 如果此位置數據為空,則key1-value1添加成功 【情況1】
② 如果此位置數據不為空,(意味者此位置上存在一個或多個數據(以鏈表形式存在)),比較key1與已經存在的一個或多個數據的哈希值,
- 如果key1的哈希值與已經存在的哈希值都不相同,此時添加成功 【情況2】
- 如果key1的哈希值與已經存在的哈希值都相同,繼續比較,調用key1所在類的equals方 法,如果equals返回false,則key1-value1添加成功;如果equals返回true,則用value1替換相同key的value值 【情況3】
補充:關於 情況2 和 情況3 :此時key1-value1 和原來的數據以 鏈表 的方式存儲。【見下圖】
在不斷的添加過程中,會涉及到擴容問題,默認的擴容方式:擴容為原來容量的2倍,並將原有的數據復制過來。
四、HashMap在JDK8中的底層原理
jdk8相較於jdk7在底層實現方面的不同:
1. new HashMap():底層沒有創建一個長度為16的數組
2. jdk8 底層的數組是:Node[],而非Entry[]
3. 首次調用put()方法時,底層創建長度為16的數組
4. 原來jdk7底層結構只有數組+鏈表,jdk8是數組+鏈表+紅黑樹
當數組的某一個索引位置上的元素以鏈表形式存在的個數 > 8,且
當前數組的長度 > 64時,此時此索引位置上的所有數據改為使用紅黑樹
存儲【方便查找】。
五、HashMap在JDK7中的底層源碼
面試題:
談談你對 HashMap 中 put/get 方法的認識?如果了解再談談HashMap的擴容機制?默認大小是多少?什么是負載因子(或填充比)?什么是吞吐臨界值(或閾值、threshold)?
HashMap源碼中的重要常量
DEFAULT_INITIAL_CAPACITY: HashMap的默認容量,16
MAXIMUM_ CAPACITY : HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR: HashMap的默認加載因子 0.75
TREEIFY_THRESHOLD: Bucket中鏈表長度大於該默認值 8,轉化為紅黑樹
UNTREEIFY_THRESHOLD: Bucket中紅 黑樹存儲的Node小於該默認值,轉化為鏈表
MIN_TREEIFY_CAPACITY:桶中的Node被樹化時最小的hash表容量。(當桶中Node的
數量大到需要變紅黑樹時,若hash表容量小於MIN_TREEIFY_CAPACITY時,此時應執行
resize擴容操作這個MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4
倍。)
table: 存儲元素的數組,總是2的n次冪
entrySet: 存儲具體元素的集
size: HashMap中 存儲的鍵值對的數量
modCount: HashMap擴 容和結構改變的次數。
threshold: 擴容的臨界值,=容量*填充因子
loadFactor: 填充因子
5.1 構造器
//DEFAULT_INITIAL_CAPACITY: HashMap的默認容量,16
//DEFAULT_LOAD_FACTOR: HashMap的默認加載因子,0.75
public HashMap() (
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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);
// 找到一個大於等於initialCapacity並且是2的指數的值作為初始容量
// 所以你如果傳進來15,那么容量為16
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;//加載因子,默認0.75
// 初始化閾值【當占用到閾值的時候擴容】
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化Entry數組
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
5.2 put方法
解析都在代碼里 ↓
public V put(K key, V value) {
//如果key為null,其實也往里面放了
if (key == null)
return putForNullKey(value);
//計算當前key的哈希值
int hash = hash(key);
// 查找對應的數組下標【hash & (length-1)】
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果在i位置上已經有元素了,對比這個位置上的所有元素
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//覆蓋原有value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 沒有在相同hash值的鏈表中找到key相同的節點
modCount++;
addEntry(hash, key, value, i); // 在i位置對應的鏈表上添加一個節點
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果數據大小已經超過閾值[16*0.75]並且數組對應的bucket不為空,則需要擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴容2倍
resize(2 * table.length);
// key為null的時,hash值設為0
hash = (null != key) ? hash(key) : 0;
// 確定是哪一個鏈表(bucket下標)
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++;
}
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//略
}
六、HashMap在JDK8的源碼分析
重要常量:
DEFAULT_INITIAL_CAPACITY: HashMap的默認容量,16
MAXIMUM_ CAPACITY : HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR: HashMap的默認加載因子0.75
TREEIFY_THRESHOLD: Bucket中鏈表長度大於該默認值,轉化為紅黑樹
面試題:為什么不在HashMap快滿的時候再擴?
盡量讓數組出現鏈表的情況少一些
6.1 構造器
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
// all other fields defaulted
//底層並沒有創建長度為16的數組
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
在JDK8中已經不是Entry數組了,而是Node數組
transient Node<K,V>[] table;
6.2 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)
//首次調用或者長度為0,則擴容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//找到在數組中存的位置,如果這個位置沒有元素
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//hash值相等
e = p;
else if (p instanceof TreeNode)
//是否紅黑樹
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//循環判斷有沒有hash相等的
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//尾插法
p.next = newNode(hash, key, value, null);
//當鏈表長度超過8時,變成紅黑樹
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;
}
}
if (e != null) { // existing mapping for key
//替換
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//當數組某個位置鏈表超過8時,且當前數組長度超過64時,做成紅黑樹
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//MIN_TREEIFY_CAPACITY=64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
七、LinkedHashMap的底層實現
LinkedHashMap 沒有重寫 put 和 putVal,而是重寫了 putVal 中調用的 newNode 方法:
//專門用了一個數據結構 LinkedHashMap.Entry<K,V> 存儲節點,並且存儲了前一個和下一個元素
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
static class Entry<K,V> extends HashMap.Node<K,V> {
//記錄前一個節點和后一個節點
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}