HashMap()采用所有的默认配置值,其中的参数值有initialCapacity:int初始化容积,默认值DEFAULT_INITIAL_CAPACITY=16,第2个参数是loadFactor:float用于设置加载因子,取值范围为0-1之间,默认值为DEFAULT_LOAD_FACTOR=0.75,值越大则存储数据越多,hash冲突的概率越高;值越小则越浪费空间,但是hash冲突的概率越低。为了减少hash冲突,引入了加载因子,加载因子表示容器中最多存储百分子多少的数据。例如默认容器为16,默认加载因子为0.75,则表示最多存储16*0.75个元素,如果大于这个值则需要进行扩容
tableSizeFor用于计算容积值,不是设置多少就是多少。就是计算一个2**n值>=设定的容积值.例如初始化容积参数值为7则实际创建容积值为8
注意:在构造器中并没有创建任何用于存储数据的集合---延迟加载,第一次存储数据时才进行空间分配
为什么初始化容积值需要转换位2的n次方?
求余的实现方法:
2%8=2 2 & (8-1) 0010 & 0111 = 0010
9%8=1 9 & (8-1) 1001 & 0111 = 0001
15%8=7 15 & (8-1) 1111 & 0111=0111
每个Node[]数组中的元素被称一个桶bucket,一个桶对应一个hash映射的值,例如0,1等,可能会出现不同的key,但是hash映射的位置相同,例如16、32等,这采用链表结构存储hash映射值相同的所有数据(JDK8+在单个链表长度大于阈值8时自动转换为红黑树,删除节点使某单个链表节点数小于阈值6时会自动从红黑树退化为链表结构)相关参数:
- capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的2倍。
- loadFactor:负载因子,默认为 0.75。
- threshold:扩容的阈值,等于 capacity * loadFactor
JDK1.8以前版本的实现HashMap底层采用的是Entry数组和链表实现。JDK1.8和以后的版本是采用Node数组(Entry)+ 单向链表 + 红黑树实现
Map主要用于存储键key值value对,根据键得到值,因此键不允许重复,但允许值重复。
HashMap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
HashMap最多只允许一条记录的键为null;允许多条记录的值为null
HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致,在JDK1.7中会出现环形链,虽然JDK1.8不会出现环形链,但是还会有rehash操作出现死循环、脏读问题、size值不准确等问题。
如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力。
如何判断环型链?
创建一个Set,然后遍历整个集合,将每个元素的key值存入set,如果要存的key值已经存储在set中,则出现环型链
Java7中使用Entry来代表每个HashMap中的数据节点,Java8中使用Node,基本没有区别,都是key,value,hash和next这四个属性,不过,Node只能用于链表的情况,红黑树的情况需要使用TreeNode。
TREEIFY_THRESHOLD为 8如果新插入的值是链表中的第 9 个会触发下面的 treeifyBin(树化操作,就是将单向链转换为红黑树),也就是将链表转换为红黑树。
JDK8+插入数据到链表的最后面,Java7是插入到链表的最前面
HashMap的put方法的具体流程
public V put(K key, V value) { 以key存储value值,返回原始位置上的value值
//先执行hash(key)根据key获取一个hash值, 参数2是要存储的key值,参数3是要存储的value,参数4表示如果当前位置已存在一个值,是否替换,false是替换,true是不替换。参数5是否在创建模式,如果为false,则表是在创建模式
return putVal(hash(key), key, value, false, true);
}
hash方法:将高位数据移位到低位进行异或计算,这是因为有些数据计算出的哈希值差异在于高位,而HashMap里的哈希寻址是忽略容量以上的高位的,可以有效地避免类似情况下的哈希碰撞
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) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 1.如果table为空或者长度为0,即没有元素,那么使用resize()方法扩容。resize方法兼顾两个职责,创建初始化数组[因为数组采用的是延迟处理]或者在容量不满足需求时进行扩容处理。插入数据if (++size > threshold)resize();进行条件扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.计算插入存储的数组索引i,此处计算方法同的indexFor()方法,如果数组为空,即不存在Hash冲突,则直接插入数组
if ((p = tab[i = (n - 1) & hash]) == null) //通过使用hash值对当前数组长度n进行求余获取key对应的下标
tab[i] = newNode(hash, key, value, null); 如果对应下标位置上的元素为null,则新创建一个Node对象存储到当前位置
else { 如果key对应位置非空
Node<K,V> e; K k;
如果当前节点上的key值和需要插入的key值相等(==或equals),则后添加的节点替换原来的节点
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) //如果当前节点上的key值和需要插入数据的key值不相等,而且当前位置上的Node对象是TreeNode类型,则确定当前桶上存放的是红黑树,则调用红黑树的处理将当前key对应的数据插入红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { 如果当前节点上的key值和需要插入数据的key值不相等,而且当前位置上的Node对象是不是红黑树
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { 如果到达链表末尾则将数据插入到链尾上
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //如果当前链表上的节点个数大于树化阈值8
treeifyBin(tab, hash); 则进行树化处理
break;
}
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for keyV oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判断集合中的总元素个数是否大于阈值,如果大于阈值则进行扩容处理
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;}
创建节点对象的方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}容量、负载因子和树化
因为容量和负载因子决定了可用的桶的数量,空桶太多了会浪费空间,如果使用太满则会严重影响操作的性能。
插入key所对应的value,首先根据key计算对应的桶位置下标(n-1)&hash,然后再判断桶上是否存储了数据。如果没有存储数据则将key和value封装为Node对象【单向链的第一个节点】,插入对应的桶位置;如果已经插入了数据【对应位置上应该有一个单向链表或红黑树,如果第一个节点为Node则是单向链,如果TreeNode则是红黑树】,则根据是否为TreeNode,进行进一步处理,如果是红黑树,则调用TreeNode中的putTreeVal方法将数据插入到红黑树中;如果是单向链表,遍历当前桶位置上的整个链表,如果某个节点上的key值和插入数据的key值相等则后盖前【key不变value变】,如果所有节点上的key值和插入数据的key值不相等,则插入当前链表的末尾,最后再判断是否需要进行树化处理
扩容发生条件为【负载因子*容量 < 元素数量】,所以预先设置的容量需要满足大于【预估元素数量/负载因子】,同时它是大于等于预先设置的容量的2的幂数
- 如果没有特殊需求不要轻易更改,JDK自带默认负载因子是适用于通用场景需求的
- 如果确实需要修改建议不要超过0.75,因为会显著增加冲突,降低hashmap性能
- 如果使用太小的负载因子,否则可能会导致更加频繁的扩容,增加性能开销,本身访问性能也会受到影响如果容量小于64只会进行简单扩容,如果容量大于64则会进行树化改造。树化处理可以避免哈希碰撞攻击
扩容处理
- 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容
- 每次扩展的时候,都是扩展2倍
- 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。