HashMap剖析
这两天对HashMap做了个回顾再吸收,烦请各路大佬指教。
HashMap的实现原理
1.HashMap基于哈希原理,通过put(key, value)和get(key)方法存储和获取对象;
2.当存储对象时,将键值对传递给put(key value)方法时,它调用键对象key的hashCode()方法来计算hashcode,然后找到bucket位置,来存储值对象value;
3.当获取对象时,也是先计算key的hashCode,找到数组中对应位置的bucket位置,然后通过key的equals()方法找到正确的键值对key-value,然后返回值对象value;
4.HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会存储在链表的下一个节点中。每个链表节点中存储键值对key-value对象,也就是当两个不通的键对象key的hashcode相同时,他们会存储在同一个bucket位置的链表(JDK8链表长度大于8变红黑树)中,取数据可通过键对象key的equals()方法用来找到正确的键值对key-value。
JDK8中HashMap源码之添加数据
详细内容解说可以看这里(有上下两部详解)
查看图例
- 计算key的hash值,算出元素在底层数组中的下标位置。
- 通过下标位置定位到底层数组里的元素(也有可能是链表也有可能是树)。
- 取到元素,判断放入元素的key是否==或equals当前位置的key,成立则替换value值,返回旧值。
- 如果是树,循环树中的节点,判断放入元素的key是否或equals节点的key,成立则替换树里的value,并返回旧值,不成立就添加到树里。
- 否则就顺着元素的链表结构循环节点,判断放入元素的key是否或equals节点的key,成立则替换链表里value,并返回旧值,找不到就添加到链表的最后。
精简一下,判断放入HashMap中的元素要不要替换当前节点的元素,key满足以下两个条件即可替换:
- hash值相等。
- ==或equals的结果为true。
JDK8中HashMap源码之获取数据
1.调用key的hashcode方法,根据返回值定位到map里数组对应的下标
2.判断这个数组下标对应的头节点是不是为null,如果是,返回null
3.如果头节点不是null,判断key值的equals方法和查询的key值对比是否为TRUE,如果是则返回这个对象的value值,否则遍历下一个节点
4.如果遍历完map中的所有节点都无法满足上面的判断,则返回null
JDK8之前HashMap底层数据结构
JDK8之前HashMap的实现是数组+链表。它之所以有相当快的查询速度是因为先通过Key计算hashCode来决定一维数组中存储的位置,而增删速度靠的是链表来保证的。
JDK8中HashMap底层数据结构
JDK8中用数组+链表+红黑树的结构来优化,链表长度大于8同时满足HashMap中元素个数大于64则变红黑树,长度小于6变回链表。
什么是Hash表
散列表(也叫哈希表),是根据关键码值(Key Value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫散列表。
hash表里可以存储元素的位置称为桶(bucket)。
什么是Hash冲突
不同的key值产生了相同的Hash地址。
Hash冲突的解决方案
1.开放地址法
探测序列,查找一个空的单元格插入。(线性探测、再平方探测、伪随机探测)
2.链地址法
对于相同的值,使用链表进行连接,使用数组存储每一个链表。(HashMap中使用的方案)
3.公共溢出区法
建立一个特殊存储空间,专门存放冲突的数据。(适用于数据和冲突较少的情况)
4.再散列法
准备若干个函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数,依次类推。
JDK7中HashMap源码之核心成员变量
1.Entry[] table-这个Entry类型的数组存储了HashMap的真正数据。
2.size-大小,代表HashMap内存储了多少个键值对。
3.capacity-容量,实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。
4.threshold-阈值和loadFactor-装载因子,threshold是通过capacity*loadFactor得到的,当size超过threshold时(刚好相等时不会扩容),HashMap会扩容,再次计算每个元素的哈希位置。
5.entrySet、keySet和values这三个都是一种视图,真正的数据都来自table。
JDK8中HashMap源码之核心成员变量
1.Node[] table-这个Node类型的数组存储了HashMap的真正数据 static class Node<K,V> implements Map.Entry<K,V>。
2.size-大小,代表HashMap内存储了多少个键值对。
3.capacity-容量,实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。
4.threshold-阈值和loadFactor-装载因子,threshold是通过capacity*loadFactor得到的,当size超过threshold时(刚好相等时不会扩容),HashMap会扩容,再次计算每个元素的哈希位置。
5.entrySet、keySet和values这三个都是一种视图,真正的数据都来自table。
6.TREEIFY_THRESHOLD-转换成红黑树的临界值,默认8。
7.UNTREEIFY_THRESHOLD-红黑树转换成链表的临界值,默认6。
8.MIN_TREEIFY_CAPACITY-最小树形化阈值,默认64
JDK8中HashMap为什么到8转为红黑树 到6转为链表
TreeNode(红黑树中)占用空间是普通Node(链表中)的两倍,为了时间和空间的权衡。
节点的分布频率会遵循泊松分布,链表长度达到8个元素的概率为0.00000006。
如果是7,则当极端情况下(频繁的插入和删除的都是同一个哈希桶)会导致频繁的树化和非树化的变换。
JDK7与JDK8中HashMap的区别
1.底层结构
JDK7是数组+链表,存储节点是Entry
JDK8则是数组+链表+红黑树,存储节点是Node
JDK8当链表长度大于8且同事HashMap中的元素个数大于64则转换为红黑树。
2.当哈希表为空时
JDK7会先调用inflateTable()初始化一个数组
JDK8是直接调用resize()扩容
3.Hash冲突时 插入数据
JDK7采用头插
JDK8会将节点插入到链表尾部
4.hash函数
JDK7中的hash函数对哈希值的计算直接使用key的hashCode值,扰动复杂
JDK8采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,结果更散列,而且扰动简单
5.扩容策略
JDK7会重新计算hash
JDK8只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话变成“原索引+oldCap”
文章总结实属不易,如有转载,烦请标明出处~