HashMap 底层实现原理
HashMap
插入
过程
1、获取数组下标
2、插入链表
获取
过程
查询速度
扩容
扩容条件
扩容方式
扩容问题
hashmap 可以将不定长的输入,通过散列算法转换成一个定长的输出,这个输出就是散列值
JDK1.7的hashMap
Hashmap源码
继承类 AbstractMap<K,V>
实现接口 Map<K,V>,Cloneable, Serializable
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
基本属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化大小 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子0.75
static final Entry<?,?>[] EMPTY_TABLE = {}; //初始化的默认数组
transient int size; //HashMap中元素的数量
int threshold; //判断是否需要调整HashMap的容量
hashMap 默认初始容量为 16, 扩容因子为 0.75
插入数据
过程:
往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的 slot。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾(jdk1.7 头插法)。
获取数组下标:
插入的键值对,获取数组的下标通过,key的hash值 & (table.length -1) 获取。
即 如果 键的 hash 值为 15,即 0000 1111。
如果,数组长度为 8,则 n -1, 后为 7,二进制为 0000 0111。
0000 1111 与 0000 0111,按位与(取模),后得到 0000 0111,十进制为 7, 为获取的数组下标,就第7个桶的位置
获取数据
过程
从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。
查询效率
如果,每个桶都只有一个元素,那查询效率为,数组的查询效率,即 O(1) 。
如果其中一个桶或以上,有多个元素,查询效率为,链表的查询效率,即 O(n) 。
数据结构
在jdk 1.7 中 HashMap采用 数组 (bucket 位桶 ) + 链表实现 ;
即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低.;
HashMap 采用 Entry 数组 存储 key-value 对, 每个键值对组成Entry实体;
Entry 类是单向链表结构,通过next 指针指向下个 Entry 实体,来解决 Hash Key 冲突问题;
存储到数组的方式
元素存储到数据的方式 通过 key的哈希值对数组长度取模得到 数组下标的位置,并存到相应的数组。
即 hash(key.hashCode())%len, 比如上述哈希表中,12%16=12, 28%16=12, 108%16=12, 140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
如果哈希值一样,则在对应的数组位置建立新节点插入,并将旧的链表放在新建节点的next的位置,即头插法(jdk8相反,新节点插入尾部)。
如下图
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
Entry( int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
加载因子
HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。
加载因子默认为0.75, 如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
扩容
扩容的条件
HashMap.Size >= Capacity * LoadFactor。
扩容方式
创建一个新的Entry空数组,长度是原数组的2倍。
遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。
index = HashCode(Key) & (Length - 1)
当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。
扩容前
扩容后
因此要使用 concurrentHashMap 可以避免这种问题。
JDK1.8的hashMap
在JDK8用的是数组+单链表+红黑树
在JDK1.7当链表长度过长的时候,查询链表中的一个元素就比较耗时,这时就引入了红黑树。
红黑树是一棵二叉树,而且属于二叉树中比较特殊的二叉搜索树。红黑树有一条特性就是从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。这一特性,确保没有一条路径会比其他路径长出两倍,因而,红黑树是接近平衡的二叉树。这就使得红黑树的时间复杂度大大降低,为O(logN)。
红黑树替代单链表会降低集合中元素的访问速度。
JDK8规定,当链表长度大于8时,由单链表转化为红黑树;而当链表长度小于6时,又由红黑树转化为单链表。
元素插入
key,经过自定义的hash算法计算,得到一个hash值,再通过这个hash值&(table.length-1)得到在数组中的index。
然后通过索引index获取到数组中对应索引处的链表。拿到链表遍历里面的节点,如果没找到与要插入的节点具有相同key的节点,那么直接在链表中插入一个新的节点。如果找到了与要插入的节点具有相同key的节点,那么就把原有的value进行覆盖。
插入元素的方式
p.next = newNode(hash, key, value, null);
新建的节点,放在了当前节点的next(即下一个位置),说明在当前节点的尾部。插入到当前节点的尾部,那当然是尾插法了
从一个空表开始,重复读入数据,生成新的节点,当读入数据存放到新节点的数据域中,然后将新的节点读入到当前链表的表尾节点之后。
HashMap 扩容机制
当 hashMap 的元素达到扩容阈值,就开始扩容。
扩容从 高位桶开始迁移,再到低位桶, 从原散列表迁移到新的散列表。 桶迁移到新的散列表的数组下标由 key的Hash & ( table.length -1 ) 获取。
------------------------------------------------------------------------------
【1】 https://www.freesion.com/article/5619192113/
【2】https://mp.weixin.qq.com/s?__biz=MzIxMjE5MTE1Nw==&mid=2653192000&idx=1&sn=118cee6d1c67e7b8e4f762af3e61643e&chksm=8c990d9abbee848c739aeaf25893ae4382eca90642f65fc9b8eb76d58d6e7adebe65da03f80d&scene=21#wechat_redirect