变量和常量
1, static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 //默认的初始容量
2, static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
3,static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子 0.75
4,static final int TREEIFY_THRESHOLD = 8; // 默认树化阈值8, 当树中的节点的个数达到这个阈值后,要考虑变为树
5,static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量
//当单个的链表的节点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的节点个数达到8,但是table的长度未达到64,先扩容
6,static final int UNTREEIFY_THRESHOLD = 6; //默认反树化阈值6, 当树种的节点的个数达到这个阈值后,要考虑变为链表
7,transient Node<K,V>[] table; // 数组
8,transient int size; 数组
9,transient int modCount;
//modCount 记录当前集合被修改的次数
(1)添加
(2)删除
这两个操作都会影响元素的个数。
当我们使用迭代器或者foreach遍历时,如果在foreach遍历时,自动调用迭代器的迭代方法
此时在遍历过程中调用了集合的add,remove方法时,modCount就会改变。
而迭代器记录的modCount是开始迭代之前的,如果两个不一致,就会报异常。
说明有两个线路(线程)同时操作集合。这种操作有风险,为了保证结果的正确性。
避免这样的情况发生,一旦发现modCount与exectedModCount不一致,立即报错。
此类的iterator和listIterator方法返回的迭代器是快速失败的:
在创建迭代器之后,除非是通过迭代器自身的remove或add方法从结构上对列表进行修改, 否则任何时间以任何方式对列表进行修改,迭代器都会抛出ConcurrentModificationException
因此,面对并发的修改,迭代器很快就会完全失败。
而不是冒着将来某个不确定时间发生任意不确定行为的风险。
10,int threshold; 阈值,当size达到阈值时,考虑扩容
11,final float loadFactor; 加载因子,影响扩容的频率
1、new HashMap()
* public HashMap() {
* //加载因子赋值为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 其他字段都是默认值
//threshold是0
//table=null
//size = 0
}
2,put(key,value)
(1) 如果第一次添加时,把table初始化为长度为16的数组,threshold = 12
(2)如果不是第一次添加
①会考虑是否key重复,那么就替换value
②如果table[i] 下面不是树 ,统计table[i]的节点,添加之前达到7,考虑树化
当单个链表的结点个数添加之前达到7,并且table的长度达到64,才会树化。
当单个链表的结点个数添加之前达到7,table的长度未达到64,先扩容。
③table[i]下面已经是树,单独处理,直接把新的映射关系连接到树的叶子节点
④添加后,size达到threshold 还要扩容, 一旦扩容,就会调整所有的映射关系位置。
HashMap
存储到HashMap中的映射关系(key,value),其中的key的hashCode值和equals方法非常重要。
1、hashCode值
hash算法是一种可以从任何数据中提取出其“指纹”的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为hash code、数据摘要或者指纹。比较出名的hash算法有MD5、SHA。hash是具有唯一性且不可逆的,唯一性是指相同的“对象”产生的hash code永远是一样的。
2、Entry数组
HashMap和Hashtable是散列表,其中维护了一个长度为2的幂次方的Entry类型的数组table,数组的每一个元素被称为一个桶(bucket),你添加的映射关系(key,value)被最终都封装为一个Map.Entry类型的对象,放到了某个table[index]桶中。使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个table[index]。
(1)数组元素类型:Map.Entry
|
JDK1.7 |
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; //...省略 } |
|
JDK1.8 |
transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; //...省略 } |
(2)初始容量:16
|
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; |
(3)扩容为原来的2倍
|
JDK1.7 |
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } |
|
JDK1.8 |
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 } //......此处省略其他代码 } |
(4)那么HashMap是如何决定某个映射关系存在哪个桶的呢?
因为hash code是一个整数,而数组的长度也是一个整数,有两种思路:
①hash code值 % table.length会得到一个[0,table.length-1]范围的值,正好是下标范围,但是用%运算,不能保证均匀存放,可能会导致某些table[index]桶中的元素太多,而另一些太少,因此不合适。
②hash code值 & (table.length-1),因为table.length是2的幂次方,因此table.length-1是一个二进制低位全是1的数,所以&操作完,也会得到一个[0,table.length-1]范围的值。
|
JDK1.7 |
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); } |
|
JDK1.8 |
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; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //....省略大量代码 } |
3、HashMap中的散列函数hash()
JDK1.7和JDK1.8关于hash()的实现代码不一样,但是不管怎么样都是为了提高hash code值与 (table.length-1)的按位与完的结果,尽量的均匀分布。
|
JDK1.7 |
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } |
|
JDK1.8 |
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } |
这里用JDK1.8的示例分析一下:
那么,JDK1.8为什么要保留高16位呢?
因为一个HashMap的table数组一般不会特别大,至少在不断扩容之前,那么table.length-1的大部分高位都是0,直接用hashCode和table.length-1进行&运算的话,就会导致总是只有最低的几位是有效的,那么就算你的hashCode()实现的再好也难以避免发生碰撞,这时保留高16位的意义就体现出来了。它对hashcode的低位添加了随机性并且混合了高位的部分特征,显著减少了碰撞冲突的发生。
4、解决冲突
虽然从设计hashCode()到上面HashMap的hash()函数,都尽量减少冲突,但是仍然存在两个不同的对象返回的hashCode值相同,或者hashCode值就算不同,通过hash()函数计算后,得到的index也会存在大量的相同,因此key分布完全均匀的情况是不存在的。那么发生碰撞冲突时怎么办?
JDK1.8之间使用:数组+链表的结构。
JDK1.8之后使用:数组+链表/红黑树的结构。
5、JDK1.7的put存储过程
(1)几个常量和变量值的作用:
①默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
②负载因子
final float loadFactor;
③阈值
int threshold;
|
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); |
|
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); //扩容 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } |
(2)存储过程
A. 先判断table是否为空数组,如果是,先初始化数组,长度为16;
B. 判断key是null,index=0;如果key不是null,那么先计算hash(key),在通过处理过的hash值&(table.length-1)计算index,决定是在table[index],index在[0,table.length-1]范围内;
C. 判断table[index]桶下是否存在某个Entry的key与新的key的“相同”(hash值相同并且(满足key的地址相同或key的equals返回true)),如果是,用新的value替换原来的value;
D. 如果不存在,判断是否满足size达到阈值(threshold)并且table[index]不是null,如果是,先扩容;扩容会导致原来table中的所有元素都会重新计算位置,并调整存储位置;
E. 添加一个新的Entry对象至table[index](注意,这个index也是重新计算过的)中,并且把当前table[index]下的所有元素都连接到新的Entry的next下。
F. size++,元素个数增加
6、JDK1.8的put存储过程
(1)几个常量和变量值的作用:
①默认负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;
②负载因子final float loadFactor;
③阈值int threshold;
当size达到threshold阈值时,会扩容;
④树化阈值static final int TREEIFY_THRESHOLD = 8;
该阈值的作用是判断是否需要树化,树化的目的是为了提高查询效率;当某个链表的结点个数达到这个值时,可能会导致树化。
⑤树化最小容量值static final int MIN_TREEIFY_CAPACITY = 64;
当某个链表的结点个数达到8时,还要检查table的长度是否达到64,如果没有达到,先扩容解决冲突问题
⑥反树化阈值static final int UNTREEIFY_THRESHOLD = 6;
当删除了结点时,如果某棵红黑树的结点个数已经低于该值时,会把树重新变成链表,目的是减少复杂度。
(2)存储过程
A. 先计算key的hash值,如果key是null,hash值就是0,如果为null,使用(h = key.hashCode()) ^ (h >>> 16)得到hash值;
B. 如果table是空的,先初始化table数组;
C. 通过hash值计算存储的索引位置index = hash & (table.length-1)
D. 如果table[index]==null,那么直接创建一个Node结点存储到table[index]中即可
E. 如果table[index]!=null,并且table[index]是一个TreeNode结点,说明table[index]下是一棵红黑树,如果该树的某个结点的key与新的key“相同”(hash值相同并且(满足key的地址相同或key的equals返回true)),那么用新的value替换原来的value,否则将(key,value)封装为一个TreeNode结点,连接到红黑树中。
F. 如果table[index]不是一个TreeNode结点,说明table[index]下是一个链表,如果该链表中的某个结点的key与新的key“相同”,那么用新的value替换原来的value,否则需要判断table[index]下结点个数,如果没有达到TREEIFY_THRESHOLD(8)个,那么(key,value)将会封装为一个Node结点直接链接到链表尾部。
G. 如果table[index]下结点个数已经达到TREEIFY_THRESHOLD(8)个,那么再判断table.length是否达到MIN_TREEIFY_CAPACITY(64),如果没达到,那么先扩容,扩容会导致所有元素重新计算index,并调整位置;
H. 如果table[index]下结点个数已经达到TREEIFY_THRESHOLD(8)个并table.length也已经达到MIN_TREEIFY_CAPACITY(64),那么会将该链表转成一棵自平衡的红黑树,并将结点链接到红黑树中。
I. 如果新增结点而不是替换,那么size++,并且还要重新判断size是否达到threshold阈值,如果达到,还要扩容。
7、关于映射关系的key是否可以修改?
映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
这个规则也同样适用于LinkedHashMap、HashSet、LinkedHashSet、Hashtable等所有散列存储结构的集合。
|
JDK1.7 |
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; //...省略 } |
|
JDK1.8 |
transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; //...省略 } |
本文详细介绍了HashMap的内部机制,包括默认初始容量、最大容量、加载因子、树化和反树化阈值。分析了put操作的流程,涉及扩容、树化条件以及键值对的存储策略。还探讨了HashMap如何处理冲突,以及key的不可变性以确保映射关系的稳定性。
7816

被折叠的 条评论
为什么被折叠?



