一:简介
1:存储格式(key,value)的方式,根据key的hashCode值存储数据。
2:允许key的值为Null。
3:是非线程安全,即任一时刻可以有多个线程同时写HashMap,在扩容的时候可能会导致数据的不一致,出现死循环,从而使CPU使用率达到100%,服务器宕机。
4:如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
ConcurrentHashMap核心数据如table、value、链表都是volatile修饰,1.7使用分段锁啊,1.8使用CAS+synchronized实现。
二:数据结构
jdk8之前hashmap的存储是数组+链表的形式,jdk8以后以数组+链表/红黑树的形式存储
三:HashMap中的相关变量
1:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
默认初始化桶的数量(容量的大小) , 1转成二进制左移四位是10000 为16
2:static final int MAXIMUM_CAPACITY = 1 << 30;
最大桶的值 1向左移动30位
3:static final float DEFAULT_LOAD_FACTOR = 0.75f
默认负载因子0.75 超过16*0.75=12后HashMap将进行扩容,桶的数量扩大2倍
4:final float loadFactor;
实际负载因子,可以new HashMap的时候传此值
5:static final int TREEIFY_THRESHOLD = 8
链表转成红黑树的阈值,在存储数据时,当链表长度 > 8时(需要结合MIN_TREEIFY_CAPACITY参数),则将链表转换成红黑树
6:static final int UNTREEIFY_THRESHOLD = 6
红黑树转成链表的阈值,当链表长度 < 6时,则将红黑树转换成链表
7:static final int MIN_TREEIFY_CAPACITY = 64;
链表转红黑树所需的最小的桶(容量)阈值:即 当哈希表中的容量 > 该值时,并且链表长度 > 8时,才允许将链表转换成红黑树。否则,先尝试扩容解决链表过长的问题,而不是树形化
8:int threshold
扩容阈值(threshold),当哈希表的大小 >扩容阈值时(if (++size > threshold) resize()),就会扩容哈希表(即扩充HashMap的容量),扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。扩容阈值 = 容量 x 加载因子。也是HashMap所能容纳的最大数据量的Node(键值对)个数
四:HashMap相关构造方法
// 可传参初始容量和负载因子,注意:此处的容量如果不是2的幂,会自动转成大于此数的最接近的2的幂
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果大于MAXIMUM_CAPACITY那么就用MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//赋值扩容量,tableSizeFor方法生成的值是指定容量的值往上找最近的2次幂的数
this.threshold = tableSizeFor(initialCapacity);
}
//默认0.75负载因子,容量可以传参
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//默认0.75负载因子,容量16
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
五:HashMap关键方法
put(K key, V value),hash(Object key),putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict),treeifyBin(Node<K,V>[] tab, int hash)
当两个node的hash值相同的时候,就会发生hash碰撞,这个时候就会判断两个node的key是否相同,相同的话覆盖,不相同则放到链表尾部,用Node中的next属性指向下一个Node。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//获取key的hash,使用key的hashCode和h >>> 16(>>>无符号向右移动16位)进行异或运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 实现了map的put和其他相关方法
*
* @param hash key的hash值
* @param key key值
* @param value 需要put的value
* @param onlyIfAbsent 如果true,那么不会覆盖已存在的值
* @param evict 如果是false,那么代表此map处于创建模式
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//node数组临时变量
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//第一次添加元素
if ((tab = table) == null || (n = tab.length) == 0)
//数组(hashmap)的长度
n = (tab = resize()).length;
//判断下一个数组位置是否有值,无的话则新增一个Node放到Node数组中,下标 i = (n - 1) & hash,n 就是HashMap中数组的长度默认为16,hash就是通过对象key产生的hashcode。也就是下标是通过数 //组长度-1 和 hashcode 按位与产生的。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else{
Node<K,V> e; K k;
//获取当前元素的hash值、key,与put的元素进行比较判断;
//这个情况是hash值相同,key值也相同的情况,put的元素覆盖当前下标的元素
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);
else {
//程序走到这,说明链表第一个节点是hash值相同,key值不相同(或者i = (n - 1) & hash计算后的下标相同,比如负载因子是1.5 容量是4,那么下标0和下标4的会在同一个链表上)
// 遍历当前链表,把当前的put的key-value封装成Node放到链表的最后(jdk1.8)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 是因为 binCount 是 0 为第一个节点
treeifyBin(tab, hash);
break;
}
//判断链表中是否由hash值相同,key相同的node
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//链表存在hash值相同,key相同的node,将新value覆盖旧value
if (e != null) { // existing mapping for key
//老值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//覆盖老值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
//创建一个比原来数组容量大两倍的新数组,遍历原来的数组,把原来数组上的元素重新按
//位与运算,放到新数组上。
resize();
afterNodeInsertion(evict);
return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//判断数组长度是否小于64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 先扩容
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//大于64 转红黑树
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
1:测试1
上面我们说到当两个node的hash值相同的时候,就会发生hash碰撞,这个时候就会判断两个node的key是否相同,相同的话覆盖,不相同则放到链表尾部
那么是否链表中值的hash是否都一样?答案不是的,如果数组长度-1和hash值与运算后产生了相同的值,此时即使hash值不一样,也会存放到同一个位置的数组上,即(n-1)&hash相同
为了方便这里使用1.5作为负载因子,初始化容量是4
public class TestHashMap {
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>(4,1.5F);
for (int i=0;i<6;i++){
int h;
Integer key =i;
int hash = (h = key.hashCode()) ^ (h >>> 16);
System.out.println("hashcode:"+key.hashCode());
System.out.println("hash:"+hash);
System.out.println("运算后下标:"+String.valueOf(3&i));
map.put(i, i);
}
}
}
当程序进行到i=4的时候,此时是第五个数,由于key是0的时候 3&0=0,此时key是4 ,3&4=0,即它们都到了下标0这个位置,由下图还发一个规律,产生的下标值和数组的长度有很大关系,当长度为4的时候,产生的下标只和后2位有关,当长度为8的时候,产生的下标只和后4位有关,以此类推。。。。。。
然后判断它们的hash和key是否相同,相同则覆盖,很明显一个Hash是0,一个hash是4,再判断它们是否是红黑树,也不是,最后走到在下标0的位置创建了链表,node的值为0的指向了node值为4的位置。i=5的时候同理,node的值为1的指向了node值为5的位置
此时的数据结构
map的键值对
因此,负载因子越低,桶的数量越大,那么发生哈希碰撞的概率就越小,效率就越高,但是越浪费内存
2:测试2
是否链表长度大于8,就开始转红黑树?
为了方便这里使用9作为负载因子,初始化容量是2
看下面的例子,当程序走到 i=16,也就是第17次进来,此时HashMap的数据结构,2个桶的链表长度都已经到了8,但是由于桶的数量小于64,此时触发了resize()方法进行了扩容,而不是转红黑树。此时桶的容量变成了原来的2倍4,threshold也由原来的18变为了36
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>(2,9);
for (int i=0;i<18;i++){
int h;
Integer key =i;
int hash = (h = key.hashCode()) ^ (h >>> 16);
System.out.println("hash:"+hash);
System.out.println("运算后下标:"+String.valueOf(3&i));
if(i==16){
System.out.println("size17");
}
map.put(i, i);
}
}
此时的数据结构
链表的数量已经大于了8(下标是0开始的)
进行HashMap扩容
i=17的时候,发现桶的数量已经扩容成了4,threshold变为了36
扩容后的数据结构,在i=16的就已经重新进行了hash运算,下标的位置也会变化,其实hashMap中的数据位置也可以看成当前的数据结构。
由此可见扩容是一个特别耗性能的操作,所以在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容
此时的数据结构