HashMap是基于哈希表实现的map,哈希表(也叫关联数组)一种通用的数据结构,是Java开发者常用的类,常用来存储和获取数据,功能强大使用起来也很方便,是居家旅行...不对,是Java开发需要掌握的基本技能,也是面试必考的知识点,所以,了解HashMap是很有必要的。
原理
简单讲解下HashMap的原理:HashMap基于Hash算法,我们通过put(key,value)存储,get(key)来获取。当传入key时,HashMap会根据key.hashCode()计算出hash值,根据hash值将value保存在bucket里。当计算出的hash值相同时怎么办呢,我们称之为Hash冲突,HashMap的做法是用链表和红黑树存储相同hash值的value。当Hash冲突的个数比较少时,使用链表,否则使用红黑树。
内部存储结构
HashMap类实现了Map< K, V>接口,主要包含以下几个方法:
- V put(K key, V value)
- V get(Object key)
- V remove(Object key)
- Boolean containsKey(Object key)
HashMap使用了一个内部类Node< K, V>来存储数据
我阅读的是Java 8的源码,在Java 8之前存储数据的内部类是Entry< K, V>,代码大体都是一样的
Node代码:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
可以看见Node类中除了键值对(key-value)以外,还有额外的两个数据:
- hash : 这个是通过计算得到的散列值
- next:指向另一个Node,这样HashMap可以像链表一样存储数据
因此可以知道,HashMap的结构大致如下:
我们可以将每个横向看成一个个的桶,每个桶中存放着具有相同Hash值的Node,通过一个list来存放每个桶。
内部变量
// 默认容量大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 转换为二叉树的阀值
static final int TREEIFY_THRESHOLD = 8;
// 转换为二叉树的最低阀值
static final int UNTREEIFY_THRESHOLD = 6;
// 二叉树最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 哈希表
transient Node<K,V>[] table;
// 键值对的数量
transient int size;
// 记录HashMap结构改变次数,与HashMap的快速失败相关
transient int modCount;
// 扩容的阈值
int threshold;
// 装载因子
final float loadFactor;
常用方法
put操作
put函数大致的思路为:
- 对key的hashCode()做hash,然后再计算index;
- 如果没碰撞直接放到bucket里;
- 如果碰撞了,以链表的形式存在buckets后;
- 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
- 如果节点已经存在就替换old value(保证key的唯一性)
- 如果bucket满了(超过load factor*current capacity),就要resize。
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)
n = (tab = resize()).length; // resize()是调整table数组大小的,如果table数组为空或长度为0,重新调整大小
if ((p = tab[i = (n - 1) & hash]) == null) // i = (n - 1) & hash | 这里计算出来的i值就是存放数组的位置,如果当前位置为空,则直接放入其中
tab[i] = newNode(hash, key, value, null);
else { // hash冲突
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 如果hash相同,并且key值也相同,则找到存放位置
e = p;
else if (p instanceof TreeNode) // 如果当前p是二叉树,则放入二叉树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 存放到链表中
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 遍历链表并将值放到链表最后
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); // 如果链表中的值大于TREEIFY_THRESHOLD - 1,则将链表转换成二叉树
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 表示对于当前key早已经存在
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) // 如果onlyIfAbsent为false或则oldValue为空,替换原来的值
e.value = value;
afterNodeAccess(e);
return oldValue; // 返回原来的值
}
}
++modCount; // HashMap结构修改次数,主要用于判断迭代器中fail-fast
if (++size > threshold) // 如果++size后的值比阀值大,则重新调整大小
resize();
afterNodeInsertion(evict);
return null;
}
代码也比较容易看懂,值得注意的就是
else if (p instanceof TreeNode) // 如果当前p是二叉树,则放入二叉树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
与
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); // 如果链表中的值大于TREEIFY_THRESHOLD - 1,则将链表转换成二叉树
这是Java 8相对于以前版本一个比较大的改变。
在Java 8以前,每次产生hash冲突,就将记录追加到链表后面,然后通过遍历链表来查找。如果某个链表中记录过大,每次遍历的数据就越多,效率也就很低,复杂度为O(n);
在Java 8中,加入了一个常量TREEIFY_THRESHOLD=8,如果某个链表中的记录大于这个常量的话,HashMap会动态的使用一个专门的treemap实现来替换掉它。这样复杂度是O(logn),比链表的O(n)会好很多。
对于前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。
get操作
在理解了put之后,get就很简单了。大致思路如下:
- bucket里的第一个节点,直接命中;
- 如果有冲突,则通过key.equals(k)去查找对应的entry
- 若为树,则在树中通过key.equals(k)查找,O(logn);
- 若为链表,则在链表中通过key.equals(k)查找,O(n)。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k)))) // 如果hash相同并且key值一样则返回当前node
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode) // 如果当前node为二叉树,则在二叉树中查找
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);
}
}
return null;
}