参考:https://juejin.im/post/599652796fb9a0249975a318#heading-5
HashMap 简介
HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。
底层数据结构
HashMap 是一个关联数组、哈希表,它是线程不安全的,允许key为null(只允许一个key为空),value为null。遍历时无序。
其底层数据结构是数组称之为哈希桶,每个桶里面放的是链表,链表中的每个节点,就是哈希表中的每个元素。
在JDK8中,当链表长度达到8,会转化成红黑树,以提升它的查询、插入效率,它实现了Map<K,V>, Cloneable, Serializable接口。
链表中的节点Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//哈希值
final K key;//key
V value;//value
Node<K,V> next;//链表后置节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//每一个节点的hash值,是将key的hashCode 和 value的hashCode 亦或得到的。
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//设置新的value 同时返回旧value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
树节点源码:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父
TreeNode<K,V> left; // 左
TreeNode<K,V> right; // 右
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; // 判断颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 返回根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
因其底层哈希桶的数据结构是数组,所以也会涉及到扩容的问题。
当HashMap的容量达到threshold域值时,就会触发扩容。扩容前后,哈希桶的长度一定会是2的次方(如果不是2的次方,优化的 hash & (table.length-1) 会发生索引越界)。
这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作( hash & (table.length-1) 替代 hash % (table.length)),更加高效。
当然如果不考虑效率用hash % (table.length)就不要求是2的次方
而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。
因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。
但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过 & 操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。
扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作。
JDK1.7 hash方法
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK1.8 hash方法
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。
扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位 = low位 + 原哈希桶容量 。 如果追加节点后,链表数量>=8,则转化为红黑树
构造函数(4种)
//最大容量 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//哈希桶,存放链表。 长度是2的N次方,或者初始化时为0.
transient Node<K,V>[] table;
//加载因子,用于计算哈希表元素数量的阈值。
final float loadFactor;
//哈希表内元素数量的阈值,当哈希表内元素数量超过阈值时,会发生扩容resize()。
int threshold; // threshold = 哈希桶.length * loadFactor;
第一种:默认构造函数,赋值加载因子为默认的0.75f
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
第二种 :指定初始化容量的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
第三种:同时指定初始化容量 以及 加载因子,用的很少,一般不会修改loadFactor
public HashMap(int initialCapacity, float loadFactor) {
//边界处理
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量最大不能超过2的30次方
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//显然加载因子不能为负数
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//设置阈值为 >=初始化容量的 2的n次方的值
this.threshold = tableSizeFor(initialCapacity);
}
//根据期望容量cap,返回2的n次方形式的 哈希桶的实际容量 length。 返回值一般会>=cap
static final int tableSizeFor(int cap) {
//经过下面的 或 和位移 运算, n最终各位都是1。
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//判断n是否越界,返回 2的n次方作为 table(哈希桶)的阈值
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
第四种:新建一个哈希表,同时将另一个map m 里的所有元素加入表中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//将另一个Map的所有元素加入表中,参数evict初始化时为false,其他情况为true
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//拿到m的元素数量
int s = m.size();
//如果数量大于0
if (s > 0) {
//如果当前表是空的
if (table == null) { // pre-size
//根据m的元素数量和当前表的加载因子,计算出阈值
float ft = ((float)s / loadFactor) + 1.0F;
//修正阈值的边界 不能超过MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果新的阈值大于当前阈值
if (t > threshold)
//返回一个 >=新的阈值的 满足2的n次方的阈值
threshold = tableSizeFor(t);
}
//如果当前元素表不是空的,但是 m的元素数量大于阈值,说明一定要扩容。
else if (s > threshold)
resize();
//遍历 m 依次将元素加入当前表中。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
put方法
HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。
1.如果定位到的数组位置没有元素 就直接插入。
2.如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
(图片来源网络,侵删)
public V put(K key, V value) {
//先根据key,取得hash值。 再调用putVal插入节点
return putVal(hash(key), key, value, false, true);
}
如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。如果evict是false。那么表示是在初始化时调用的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
}
putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab存放 当前的哈希桶, p用作临时链表节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果当前哈希表是空的,代表是初始化
if ((tab = table) == null || (n = tab.length) == 0)
//那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n
n = (tab = resize()).length;
//如果当前index的节点是空的,表示没有发生哈希碰撞。 直接构建一个新节点Node,挂载在index处即可。
//这里再啰嗦一下,index 是利用 哈希值 & 哈希桶的长度-1,替代模运算
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//否则 发生了哈希冲突。
//e
Node<K,V> e; K k;
//如果哈希值相等,key也相等,则是覆盖value操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//将当前节点引用赋值给e
else if (p instanceof TreeNode)//红黑树暂且不谈
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);
//如果追加节点后,链表数量》=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;
}
}
//如果e不是null,说明有需要覆盖的节点,
if (e != null) { // existing mapping for key
//则覆盖节点值,并返回原oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeAccess(e);
return oldValue;
}
}
//如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。
//修改modCount
++modCount;
//更新size,并判断是否需要扩容。
if (++size > threshold)
resize();
//这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeInsertion(evict);
return null;
}