一, 什么是HashMap
image-20210212112919731Java中Map是用来存储键值对<key,value>的集合类,也就是哈希表,而HashMap是Map的实现类.
具有存储效率高,查询快的特点,但不是线程同步的,按照哈希表的特点,Map中的key是不能重复的.
二, 实现原理
HashMap采用数组+链表+红黑树实现
每个数组空间采用Node<key,value>节点来保存键值对,在一定的条件下会采用TreeNode<key,value>保存数据.
image-20210212112919731
三, 基本属性介绍
//The default initial capacity - MUST be a power of two //默认初始容量-必须是2的幂. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
//The maximum capacity, used if a higher value is implicitly specified by either of the constructors with arguments.MUST be a power of two <= 1<<30. //哈希数组最大容量,如果构造函数制定了更大的值,则只使用最大值,并且大小只能为2的幂 static final int MAXIMUM_CAPACITY = 1 << 30
//The load factor used when none specified in constructor //构造函数中未指定时使用的负载系数(默认使用) static final float DEFAULT_LOAD_FACTOR = 0.75f
//The bin count threshold for using a tree rather than list for a bin. //Bins are converted to trees when adding an element to a bin with at least this many nodes. //The value must be greater than 2 and should be at least 8 to mesh with assumptions in tree removal about //conversion back to plain bins upon shrinkage. //树化阈值: 当一个数组框中的节点数量大于TREEIFY_THRESHOLD时应当采用树来存储,而不是链表 static final int TREEIFY_THRESHOLD = 8
//The bin count threshold for untreeifying a (split) bin during a resize operation. //Should be less than TREEIFY_THRESHOLD, and at most 6 to mesh with shrinkage detection under removal. //树节点数量小于6个时,则将树转换为链表 static final int UNTREEIFY_THRESHOLD = 6
//The smallest table capacity for which bins may be treeified. //树化的最小阈值: 只有哈希表中的节点数量大于64时,才能将链表转换为树 static final int MIN_TREEIFY_CAPACITY = 64
//存储节点的哈希数组 transient Node<K, V>[] table
//哈希表中的节点数量 transient int size
//修改HashMap的次数,用于fail-fast transient int modCount
//The next size value at which to resize (capacity * load factor) //下一次需要扩容的界限(扩容阈值) int threshold
//The load factor for the hash table //负载系数,主要用来调节哈希表的填充度 final float loadFactor
四, 构造函数
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);
this.loadFactor = loadFactor;
//设置下一次扩容的阈值,这里不是真正的阈值,后面会重新设置
this.threshold = tableSizeFor(initialCapacity);
}
五, 常用方法介绍
/**
* 计算传入值的hash值,用来计算存储的位置
* 将hashCode的高16位与低16位进行异或操作,这样做的原因主要是为了更好的分散hash分布,减少哈希碰撞
* 如果采用&则计算出来的结果会向0靠近,采用|计算的结果会向1靠近,都不能做到均匀分布
* 做异或操作的原因: 加大对哈希码低位的随机性
* String s = "Hello World!";
* Integer.toBinaryString(s.hashCode())
* h.hashCode() = 1100 0110 0011 1100 1011 0110 0001 1101
* h.hashCode() >>> 16 = 0000 0000 0000 0000 1100 0110 0011 1100
* 1100 0110 0011 1100 1011 0110 0001 1101 ^
* 0000 0000 0000 0000 1100 0110 0011 1100
* 1100 0110 0011 1100 1000 0110 0001 1100
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 刚方法主要用来计算哈希表的大小,从上面的属性可知,哈希表的大小始终为2的幂
* 这样做的原因是:
* 1. length的二进制表示始终为100...000,length - 1的二进制为1111...111
* 2. 长度等于2的幂时,hash%length = hash&(length - 1),但后者比前者效率更高
* 3. 可以保证计算的位置更加的分散,如果长度为奇数,则length - 1为偶数,无论怎么取余或&操作,最终结果都为偶数,会造成一半空间浪费
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
获取节点
image-20210212125544816final 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 && ((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);
}
}
return null;
}
//查找节点
final TreeNode<K, V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
//获取树的根节点
final TreeNode<K, V> root() {
for (TreeNode<K, V> r = this, p; ; ) {
//只有根节点的父节点为空
if ((p = r.parent) == null)
return r;
//向父节点移动
r = p;
}
}
//在树中查找
final TreeNode<K, V> find(int h, Object k, Class<?> kc) {
TreeNode<K, V> p = this; //复制当前节点
do {
int ph, dir; //当前节点的hash值,方向
K pk; //当前节点的key
TreeNode<K, V> pl = p.left, pr = p.right, q; //当前节点的左孩子,右孩子,以及q来返回查找到的结果
//当前节点的hash值大于需要找的节点的hash值,需要向左子树移动
if ((ph = p.hash) > h)
p = pl;
//当前节点的hash值小于需要找的节点的hash值,需要向右子树移动
else if (ph < h)
p = pr;
//当前节点的hash值相同,并且key也相同,直接返回
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//左子树为空,向右子树移动查找
else if (pl == null)
p = pr;
//右子树为空,向左子树移动判断
else if (pr == null)
p = pl;
//这一步主要通过Comparable对比当前遍历的节点p的key和要查找的key的大小
else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr; //小于0向左遍历,大于0向右遍历
//这一步表示无法通过Comparable比较大小,则从右孩子继续递归遍历查找
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
//从右孩子递归遍历后还没有找到,从左孩子继续查找
p = pl;
} while (p != null);
return null;
}
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 : ((Comparable) k).compareTo(x));
}
插入节点
putfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
/*
* 1, 如果哈希表为空,则通过resize()创建.所以,初始化链表的时机为第一次调用put函数时
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*
* 判断是否存在hash冲突:
* 若不存在,则直接在该数组位置新建节点,直接插入
* 否则,代表hash冲突,即当前存储位置已存在节点,则依次向下判断
* a. 当前位置的key与要存入位置的key相同
* b. 判断需插入的数据结构是否为红黑树或链表
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K, V> e; //待修改的节点
K k;
/*
* a. 判断table[i]的元素的key是否与需插入的key一样,如果一样直接用新的value覆盖旧的value
*/
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
/*
* b. 判断需要插入的是否为红黑树,如果为红黑树则直接在树中插入或更新键值对
*/
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
/*
* c. 如果是链表,则直接在链表中更新键值对
* i. 遍历table[i],判断key是否存在,采用equals对比当前遍历节点的key与待插入数据的key,如果相同则直接用新的value覆盖旧value
* ii. 遍历完毕没有上述情况,则直接在链表尾部插入新的节点
* tips: 新增节点之后,需要判断链表的长度是否大于8,如果是,需要树化
*/
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;
}
//如果找到了key相同的节点,则直接退出
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
//向后移动节点进行遍历
p = e;
}
}
/*
* 找到需要修改的node则直接用新值代替旧值
*/
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//默认实现为空,为了给LinkedHashMap来实现有序map
afterNodeAccess(e);
return oldValue;
}
}
//修改次数+1,用于fail-fast
++modCount;
//节点数量大于阈值,需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize
final Node<K, V>[] resize() {
//扩容前哈希表
Node<K, V>[] oldTab = table;
//旧哈希表长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//扩容前哈希表扩容阈值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//旧容量大于最大值,直接返回旧的哈希表
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
} else if
//旧容量小于最大容量的一半,并且哈希表已经初始化了则新阈值为旧阈值的一倍
((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
} else if (oldThr > 0)
// 如果旧的阈值大于0,则表示哈希表已经被初始化了,则新的容量等于旧的阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//初始化
newCap = DEFAULT_INITIAL_CAPACITY;
//新的阈值等于初始容量*加载因子
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的阈值等于0
if (newThr == 0) {
//计算新的阈值
float ft = (float) newCap * loadFactor;
//新的容量大于最大值并且新的阈值大于最大值,则新的阈值为最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
}
//赋值新的阈值
threshold = newThr;
//创建新的哈希表,赋值新的哈希表
@SuppressWarnings({"all"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;
if (oldTab != null) {
//将每个bucket中都移动到新的bucket中
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
if ((e = oldTab[j]) != null) {
//旧节点置空
oldTab[j] = null;
//当前节点没有后置节点,直接计算位置插入
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if
//当前节点为树节点,需要插入树节点并且调节树平衡
(e instanceof TreeNode)
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else {
// 处理链表节点
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
next = e.next;
//原位置不需要移动
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
//尾插法
loTail.next = e;
//尾节点向后移动
loTail = e;
} else {
//需要重新计算插入位置
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
//尾节点向后移动
hiTail = e;
}
} while ((e = next) != null);
//原索引放在bucket中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//原索引+oldCap放到bucket中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
五, 与之前版本对比
1. hash值计算方式
//1.7
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//1.8
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
从上可以看出1.7 一共做了5次异或操作和4次无符号右移操作,而1.8一共做了一次右移和一次异或,效率比1.8高
2. 存储数据结构不同
1.7 采用数组 + 链表解决哈希冲突,链表采用头插法
1.8 采用数组 + 链表 + 红黑树 解决哈希冲突,链表采用尾插法
并且1.7的数据结构还会造成循环链表
问题产生的条件:
多线程同时进行put时,如果同时调用了resize操作,可能会导致循环链表产生,进而会导致后面get的时候产生死循环
问题产生的原因:
多线程下进行put,原本应该在当前节点的next被其他线程移到新数组的空位置上,而当前节点的next指针指向刚放入的节点,从而造成循环链表.
3. 扩容机制不同
1.7 对原先的每一个节点重新计算插入位置
1.8 只有第一个位置上的节点不需要重新计算插入位置,其他的都为当前位置+旧数组容量
网友评论