对于HashMap来说,底层是基于散列算法实现,散列算法分为散列再探测和拉链式。HashMap 则使用了拉链式的散列算法,即采用数组+链表/红黑树来解决hash冲突,数组是HashMap的主体,链表主要用来解决哈希冲突。这个数组是Entry类型,它是HashMap的内部类,每一个Entry包含一个key-value键值对
下面是JDK 1.8中HashMap的get方法的简要实现过程:
public V get(Object key) {
Node<K, V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get 方法看起来很简单,就是通过同样的 hash 得到 key 的hash 值。重点看下 getNode方法:
final Node<K, V> getNode(int hash, Object key) {
//当前HashMap的散列表的引用
Node<K, V>[] tab;
//first:桶头元素
//e:用于存放临时元素
Node<K, V> first, e;
//n:table 数组的长度
int n;
//元素中的 k
K k;
// 将 table 赋值为 tab,不等于null 说明有数据,(n = tab.length) > 0 同理说明 table 中有数据
//同时将 该位置的元素 赋值为 first
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
//定位到了桶的到的位置的元素就是想要获取的 key 对应的,直接返回该元素
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) {
return first;
}
//到这一步说明定位到的元素不是想要的,且该位置不仅仅有一个元素,需要判断是链表还是树
if ((e = first.next) != null) {
//是否已经树化
if (first instanceof TreeNode) {
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
}
//处理链表的情况
do {
//如果遍历到了就直接返回该元素
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
return e;
}
} while ((e = e.next) != null);
}
}
//遍历不到返回null
return null;
}
下面是JDK 1.8中HashMap的put方法的简要实现过程:
put方法的代码很简单,就一行代码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
核心其实是通过 putValue
方法实现的,在传给putValue
的参数中,先调用hash
获取了一下hashCode。
putVal 方法主要实现如下,给大家增加了注释:
/**
* Implements Map.put and related methods.
*
* @param hash key 的 hash 值
* @param key key 值
* @param value value 值
* @param onlyIfAbsent true:如果某个 key 已经存在那么就不插了;false 存在则替换,没有则新增。这里为 false
* @param evict 不用管了,我也不认识
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab 表示当前 hash 散列表的引用
Node<K, V>[] tab;
// 表示具体的散列表中的元素
Node<K, V> p;
// n:表示散列表数组的长度
// i:表示路由寻址的结果
int n, i;
// 将 table 赋值发给 tab ;如果 tab == null,说明 table 还没有被初始化。则此时是需要去创建 table 的
// 为什么这个时候才去创建散列表?因为可能创建了 HashMap 时候可能并没有存放数据,如果在初始化 HashMap 的时候就创建散列表,势必会造成空间的浪费
// 这里也就是延迟初始化的逻辑
if ((tab = table) == null || (n = tab.length) == 0) {
n = (tab = resize()).length;
}
// 如果 p == null,说明寻址到的桶的位置没有元素。那么就将 key-value 封装到 Node 中,并放到寻址到的下标为 i 的位置
if ((p = tab[i = (n - 1) & hash]) == null) {
tab[i] = newNode(hash, key, value, null);
}
// 到这里说明 该位置已经有数据了,且此时可能是链表结构,也可能是树结构
else {
// e 表示找到了一个与当前要插入的key value 一致的元素
Node<K, V> e;
// 临时的 key
K k;
// p 的值就是上一步 if 中的结果即:此时的 (p = tab[i = (n - 1) & hash]) 不等于 null
// p 是原来的已经在 i 位置的元素,且新插入的 key 是等于 p中的key
//说明找到了和当前需要插入的元素相同的元素(其实就是需要替换而已)
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
//将 p 的值赋值给 e
e = p;
//说明已经树化,红黑树会有单独的文章介绍,本文不再赘述
else if (p instanceof TreeNode) {
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
} else {
//到这里说明不是树结构,也不相等,那说明不是同一个元素,那就是链表了
for (int binCount = 0; ; ++binCount) {
//如果 p.next == null 说明 p 是最后一个元素,说明,该元素在链表中也没有重复的,那么就需要添加到链表的尾部
if ((e = p.next) == null) {
//直接将 key-value 封装到 Node 中并且添加到 p的后面
p.next = newNode(hash, key, value, null);
// 当元素已经是 7了,再来一个就是 8 个了,那么就需要进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
break;
}
//在链表中找到了某个和当前元素一样的元素,即需要做替换操作了。
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
//将e(即p.next)赋值为e,这就是为了继续遍历链表的下一个元素(没啥好说的)下面有张图帮助大家理解。
p = e;
}
}
//如果条件成立,说明找到了需要替换的数据,
if (e != null) {
//这里不就是使用新的值赋值为旧的值嘛
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
//这个方法没用,里面啥也没有
afterNodeAccess(e);
//HashMap put 方法的返回值是原来位置的元素值
return oldValue;
}
}
// 上面说过,对于散列表的 结构修改次数,那么就修改 modCount 的次数
++modCount;
//size 即散列表中的元素的个数,添加后需要自增,如果自增后的值大于扩容的阈值,那么就触发扩容操作
if (++size > threshold) {
resize();
}
//啥也没干
afterNodeInsertion(evict);
//原来位置没有值,那么就返回 null 呗
return null;
}
先通过 (table.length - 1) & (key.hashCode ^ (key.hashCode >>> 16))
定位到key位于哪个table中,然后再通过key.equals(rowKey)
来判断两个key是否相同,综上,是先通过hashCode和equals来定位KEY的。
源码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// ...省略
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 这里会通过equals判断
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// ...省略
return null;
}
所以,在使用HashMap的时候,尽量用String和Enum等已经实现过hashCode和equals方法的官方库类,如果一定要自己的类,就一定要实现hashCode和equals方法
通过源码发现,hashMap定位tableIndex的时候,是通过(table.length - 1) & (key.hashCode ^ (key.hashCode >> 16))
,而不是常规的key.hashCode % (table.length)
呢?
X % 2^n = X & (2^n – 1)
。这也是hashMap每次扩容都要到2^n的原因之一HashMap对key=null的case做了特殊的处理,key值为null的kv对,总是会放在数组的第一个元素中,如下源码所示:
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
HashMap的key和value都可以为null,优点很明显,不会因为调用者的粗心操作就抛出NPE这种RuntimeException,但是缺点也很隐蔽,就像下面的代码一样:
// 调用远程RPC方法,获取map
Map<String, Object> map = remoteMethod.queryMap();
// 如果包含对应key,则进行业务处理
if(map.contains(KEY)) {
String value = (String)map.get(KEY);
System.out.println(value);
}
虽然map.contains(key)
,但是map.get(key)==null
,就会导致后面的业务逻辑出现NPE问题