一.前言
HashMap也是我們使用非常多的Collection,它是基於哈希表的 Map 接口的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,系統會根據hash算法來來計算key-value的存儲位置,我們總是可以通過key快速地存、取value。
二.特點和常見問題
- 非線程安全
- hashMap的映射不是有序的
- key、value都可以為null
- key 不能重復
二.接口定義
查看源碼. HashMap實現了Map接口,繼承AbstractMap。
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
三.初始化構造函數
HashMap提供了三個構造函數:
1.HashMap();
構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
* 一個空的構造方法,默認初始化容量16,負載因子0.75
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
負載因子: loadFactor是map的負載因子,要大於0,且是非無窮大的數字.
有這樣一個公式:initailCapacity*loadFactor=HashMap的容量.
比如,泛型是String,String 類型的空的HashMap.(泛型: 集合內元素的類型)
HashMap map1 = new HashMap<String,String>();
2.HashMap(int initialCapacity);
構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
比如初始一個容量為5的hashMap集合.
HashMap map2 = new HashMap<String,String>(5);
System.out.println(map2.size());// 輸出0, 此時數組容量為5, 但內部元素為0.
3.HashMap(int initialCapacity, float loadFactor);
構造一個帶指定初始容量和加載因子的空 HashMap。
默認負載因子是0.75, HashMap提供可修改的負載因子的構造方法.
HashMap map2 = new HashMap<String,String>(5,0.5f);
四.HashMap內部結構
數據結構的物理存儲結構只有兩種:順序存儲結構和鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),在數組中根據下標查找某個元素,一次定位就可以達到,哈希表利用了這種特性,哈希表的主干就是數組。
簡單概括主干是數組, 每個數組元素內容部是鏈表.
源碼如下:
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能<0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: "
+ initialCapacity);
//初始容量不能 > 最大容量值,HashMap的最大容量值為2^30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//負載因子不能 < 0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: "
+ loadFactor);
// 計算出大於 initialCapacity 的最小的 2 的 n 次方值。
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
//設置HashMap的容量極限,當HashMap的容量達到該極限時就會進行擴容操作
threshold = (int) (capacity * loadFactor);
//初始化table數組
table = new Entry[capacity];
init();
}
從源碼中可以看出,每次新建一個HashMap時,都會初始化一個table數組。table數組的元素為Entry節點。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
.......
}
其中Entry為HashMap的內部類,它包含了鍵key、值value、下一個節點next,以及hash值,這是非常重要的,正是由於Entry才構成了table數組的項為鏈表。
隨着HashMap中元素的數量越來越多,發生碰撞的概率就越來越大,所產生的鏈表長度就會越來越長,這樣勢必會影響HashMap的速度,為了保證HashMap的效率,系統必須要在某個臨界點進行擴容處理。該臨界點在當HashMap中元素的數量等於table數組長度*加載因子。但是擴容是一個非常耗時的過程,因為它需要重新計算這些數據在新table數組中的位置並進行復制處理。所以如果我們已經預知HashMap中元素的個數,那么預設元素的個數能夠有效的提高HashMap的性能。
五.HashMap的存儲分析
源碼如下:
public V put(K key, V value) {
//當key為null,調用putForNullKey方法,保存null與table第一個位置中,這是HashMap允許為null的原因
if (key == null)
return putForNullKey(value);
//計算key的hash值
int hash = hash(key.hashCode()); ------(1)
//計算key hash 值在 table 數組中的位置
int i = indexFor(hash, table.length); ------(2)
//從i出開始迭代 e,找到 key 保存的位置
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
//判斷該條鏈上是否有hash值相同的(key相同)
//若存在相同,則直接覆蓋value,返回舊value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value; //舊值 = 新值
e.value = value;
e.recordAccess(this);
return oldValue; //返回舊值
}
}
//修改次數增加1
modCount++;
//將key、value添加至i位置處
addEntry(hash, key, value, i);
return null;
}
分析下源碼中保存數據的過程為:
首先判斷key是否為null,若為null,則直接調用putForNullKey方法。
若不為空則先計算key的hash值,然后根據hash值搜索在table數組中的索引位置,如果table數組在該位置處有元素,則通過比較是否存在相同的key,若存在則覆蓋原來key的value,否則將該元素保存在鏈頭(最先保存的元素放在鏈尾)。
若table在該處沒有元素,則直接保存。
HashMap中不存在相同的Key, 先看迭代處。此處迭代原因就是為了防止存在相同的key值,若發現兩個hash值(key)相同時,HashMap的處理方式是用新value替換舊value,這里並沒有處理key,這就解釋了HashMap中沒有兩個相同的key。
再來分析下添加一個新key-value 的內部實現:
void addEntry(int hash, K key, V value, int bucketIndex) {
//獲取bucketIndex處的Entry
Entry<K, V> e = table[bucketIndex];
//將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
//若HashMap中元素的個數超過極限了,則容量擴大兩倍
if (size++ >= threshold)
resize(2 * table.length);
}
這是一個非常優雅的設計。系統總是將新的Entry對象添加到bucketIndex處。如果bucketIndex處已經有了對象,那么新添加的Entry對象將指向原有的Entry對象,形成一條Entry鏈,但是若bucketIndex處沒有Entry對象,也就是e==null,那么新添加的Entry對象指向null,也就不會產生Entry鏈了
六.HashMap的讀取分析
相對於HashMap的存而言,取就顯得比較簡單了。通過key的hash值找到在table數組中的索引處的Entry,然后返回該key對應的value即可。
public V get(Object key) {
// 若為null,調用getForNullKey方法返回相對應的value
if (key == null)
return getForNullKey();
// 根據該 key 的 hashCode 值計算它的 hash 碼
int hash = hash(key.hashCode());
// 取出 table 數組中指定索引處的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key與查找的key相同,則返回相對應的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
能夠根據key快速的取到value除了和HashMap的數據結構密不可分外,還和Entry有莫大的關系,在前面就提到過,HashMap在存儲過程中並沒有將key,value分開來存儲,而是當做一個整體key-value來處理的,這個整體就是Entry對象。同時value也只相當於key的附屬而已。在存儲的過程中,系統根據key的hashcode來決定Entry在table數組中的存儲位置,在取的過程中同樣根據key的hashcode取出相對應的Entry對象。
七.常用方法
1.put(K key, V value)
將鍵(key)/值(value)映射存放到Map集合中。若key已經存在,則更新替換掉舊值.
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("key1", "val01");
map1.put("key2", "val02");
System.out.println(map1.get("key1"));
System.out.println(map1.get("b"));
map1.put("key1", "updated");
System.out.println(map1.get("key1"));
輸出結果
val01
null
updated
2.putAll(Map<? extends K, ? extends V> m)
合並集合,把參數集合中的元素合並到原來集合中, 若參數集合中的key 與原來集合中有重復, 則更新覆蓋原來集合對應key 的value.
// 兩個map具有不同的key
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "A");
HashMap<String, String> map2 = new HashMap<String, String>();
map2.put("2", "B");
map2.put("3", "C");
map1.putAll(map2);
System.out.println(map1);
// 兩個map具有重復的key
HashMap<String, String> map3 = new HashMap<String, String>();
map3.put("1", "A");
HashMap<String, String> map4 = new HashMap<String, String>();
map4.put("1", "B");
map4.put("3", "C");
map3.putAll(map4);
System.out.println(map3);
輸出結果:
{1=A, 2=B, 3=C}
{1=B, 3=C}
2.get(Object key)
返回指定鍵所映射的值,沒有該key對應的值則返回 null。
HashMap<String,String> map2=new HashMap<String,String>();
map2.put("key1","val01");
map2.put("key2", "val02");
System.out.println(map2.get("key1"));
System.out.println(map2.get("key2"));
輸出結果:
val01
val02
3.size()
返回Map集合中數據數量。
HashMap<String,String> map2=new HashMap<String,String>();
map2.put("key1","val01");
map2.put("key2", "val02");
System.out.println(map2.size());
輸出結果:
2
4.clear()
清空Map集合
HashMap<String,String> map3=new HashMap<String,String>();
map3.put("key1","val01");
map3.put("key2", "val02");
System.out.println(map3.size());
map3.clear();
System.out.println(map3.size());
輸出結果:
2
0
5.isEmpty ()
判斷Map集合中是否有數據,如果沒有則返回true,否則返回false; 觀察源碼,僅判斷元素個數是否為0, 不能判斷null.
public boolean isEmpty() {
return size == 0;
}
6.remove(Object key)
刪除Map集合中鍵為key的數據並返回其所對應value值。
HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.remove("key1"));
System.out.println(map4.size());
輸出結果:
val01
1
7.values()
返回Map集合中所有value組成的以Collection數據類型格式數據。
HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.values());
輸出結果:
[val01, val02]
8.keySet()
返回Map集合中所有key組成的Set集合
HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.keySet());
輸出結果:
[key1, key2]
9.containsKey(Object key)
判斷集合中是否包含指定鍵,包含返回 true,否則返回false.
HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.containsKey("key1"));
System.out.println(map4.containsKey("key3"));
輸出結果:
true
false
10.containsValue(Object value)
判斷集合中是否包含指定值,包含返回 true,否則返回false。
HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.containsValue("val01"));
System.out.println(map4.containsValue("val03"));
輸出結果:
true
false
八.HashMap 的java8 新特性
1.特性
-
非線程安全
-
hashMap的映射不是有序的
-
key、value都可以為null
HashMap<String, String> map1 = new HashMap<String, String>(); map1.put(null, null); System.out.println(map1);
輸出結果:
{null=null}
2.V replace(K key, V value)
替換指定key 的value, 並返回替換前的value.
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
System.out.println(map1);
System.out.println(map1.replace("1", "aReplace"));
System.out.println(map1);
輸出結果
{1=a, 2=b, 3=c}
a
3.boolean replace(key, oldValue, newValue)
用newValue 替換指定key 的oldValue; 當key 不存在 | 對應key的value 和oldValue 不相等 則返回false;
源碼如下:
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
例子:
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
// key 不存在
System.out.println(map1.replace("4", "bb", "newValue"));
// 參數oldValue 和對應key 的值不相等
System.out.println(map1.replace("2", "bb", "newValue"));
System.out.println(map1);
// 條件滿足
System.out.println(map1.replace("2", "b", "newValue"));
System.out.println(map1);
輸出結果:
false
false
{1=a, 2=b, 3=c}
true
{1=a, 2=newValue, 3=c}
4.void replaceAll(function)
function, java8的流式寫法, 個人感覺個replaceAll 沒一毛錢關系, function由我們自定義的.
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
map1.replaceAll((k,v) -> {
System.out.println(k);
System.out.println(v);
return null;
});
(k,v) k v 對應HashMap 中的key 和value. 內部是一次遍歷操作.
輸出結果:
1a2b3c
5.putIfAbsent(key, value)
如果傳入的key 已經存在, 則返回存在key 的value, 不進行更新替換value;
如果傳入的key 不存在, 則和put 作用相同, 添加新的key 和value; 並且固定返回null;
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
System.out.println("執行前: " + map1);
// 1.key不存在
System.out.println(map1.putIfAbsent("4", "d"));
// 2.key存在, value不同
System.out.println(map1.putIfAbsent("1", "aa"));
System.out.println("執行后: " + map1);
輸出結果:
執行前: {1=a, 2=b, 3=c}
null
a
執行后: {1=a, 2=b, 3=c, 4=d}
6.getOrDefault
作用是根據傳入的key 獲取value; 若Map 中沒有這個key 則返回指定的defaultValue; 若有,則相當於get方法.
源碼如下, 內部采用三元運算符進行判斷key 是否存在決定返回的value.
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
比如:
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
System.out.println(map1.getOrDefault("4", "d"));
System.out.println(map1.getOrDefault("1", "a"));
輸出結果:
d
a
7.merge
如果key存在,則執行lambda表達式,並將表達式的值更新到對應的key 中. 表達式入參為oldVal和newVal(neVal即merge()的第二個參數)。表達式返回最終put的val。如果key不存在,則直接putnewVal.
比如:
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
String retVal = map1.merge("1", "A", (oldVal, newVal) -> oldVal + newVal);
System.out.println(retVal);
System.out.println(map1);
輸出結果:
aA
{1=aA, 2=b, 3=c}
作用等同於下段代碼(JDK1.7以前版本)
if(map.containsKey(k)) {
map.put(k, map.get(k) + newVal);
} else {
map.put(k, newVal);
}
源碼如下, 解釋見注釋部分.
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
// 刪除無關代碼
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null; // 該key原來的節點對象
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0) //第一個if,判斷是否需要擴容
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
// 第二個if,取出old Node對象
// 繼續省略
}
if (old != null) {// 第三個if,如果 old Node 存在
V v;
if (old.value != null)
// 如果old存在,執行lambda,算出新的val並寫入old Node后返回。
v = remappingFunction.apply(old.value, value);
else
v = value;
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
return v;
}
if (value != null) {
//如果old不存在且傳入的newVal不為null,則put新的kv
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
// 省略
}
return value;
}
8.compute()
根據已知的 k v 算出新的v並put。如果無此key,那么oldVal為null,lambda中涉及到oldVal的計算會報空指針。源碼和merge大同小異,就不放了。
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
String retVal = map1.compute("1", (key, oldVal) -> oldVal + "AAA");
System.out.println(retVal);
System.out.println(map1);
輸出結果:
aAAA
{1=aAAA, 2=b, 3=c}
和merge 方法相比, 功能類似, 區別在於傳入的參數不同;
9.compute() 的補充方法
1) computeIfAbsent()
當key不存在時,才compute[見8]. 類似compute ,故偷個懶不作例子了
2) computeIfPresent()
當key存在時,才compute[見8].類似compute ,故偷個懶不作例子了