HashMap的底层原理
文章目录
简述
HashMap继承自AbstractMap类,实现了Map,Cloneable,Serializable接口,元素以是以key-value(键值对)存储形式存在,允许null键和null值,查询慢,增删快,线程不安全,效率高。在HashMap中,初始化一个数组长度为16,默认加载因子为0.75。在创建一个map对象后,调用put方法,传入key值及value值,首先将key值进行hash运算得到的hash值作为该entry键值对在数组中的索引位置。
此时确定该数组位置后,首先判断该位置是否为null。如果为null,则将entry存储在该位置;如果不为null,即产生hash冲突(碰撞)现象("拉链法"解决冲突),将entry以链表的方式存储在数组中,此时,通过equals()方法判断该位置的元素的key值与要存入的entry键值对的key值比较是否相等,若相等,覆盖value值存入,若不相等,则直接存入。
jdk1.8之后,当链表长度大于8且当前数组的长度大于64时时,将链表结构转为红黑树继续存储entry。
(补充:将链表转换为红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表转换为红黑树。而是选择进行数组扩容(2倍)。)
提示:以下是本篇文章正文内容,下面内容仅供参考
问题剖析
1.hashmap的底层数据结构1.8之前和1.8的区别?
Jdk1.8之前,HashMap底层基于数组和链表实现,采用Entry[ ]的table数组存储。在创建Hashmap时创建数组
Jdk1.8中,HashMap底层基于数组、链表、红黑树实现,采用Node[ ]的table数组存储。在添加元素时创建数组。jdk1.8之后,当链表长度大于8且当前数组的长度大于64时时,将链表结构转为红黑树继续存储entry。
2.hashmap的构造函数哪些?
//构造一个空的HashMap,默认初始容量为(16)和默认加载因子(0.75)
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 将默认的加载因子0.75赋值给loadFactor,并没有创建数组
}
//构造一个具有指定的初始容量和默认加载因子(0.75)HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造一个具有指定的初始容量和指定加载因子的HashMap
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
//tableSizeFor(initialCapacity)判断初始化容量是否为2的n次幂,如果不是变为比指定初始化容量大的最小的2的n次幂
}
3.hashMap 加载因子为什么0.75
//哈希表的加载因子
final float loadFactor;
加载因子过小,存放数据较为稀疏,查询元素效率低,会造成数组空间浪费。
加载因子过大,链表长度较长,元素存储较为密集,这时扩容,新建数组,进行数据复制,非常消耗性能。
例如:
(1)加载因子是0.4 。那么160.4----->6 如果数组中满6个空间就进行扩容会造成数组利用率太低了
(2)加载因子是0.9。那么160.9------->14那么这样就会导致链表有点多了。导致查找元素效率低。
所以既兼顾数组利用率又考虑链表不要太多,经过大量测试0.75是最佳方案。
4.hashmap 为什么长度是2的n次方
当我们根据key的hash计算,确定其在数组的位置时,(算法:使用hash&(length-1),而实际上hash%length等于hash&(length-1)的前提是length是2的n次幂。)
如果n为2的幂次方,可以保证数据的均匀插入,如果n不是2的幂次方,可能数组的一些位置永远不会插入数据,浪费数组空间,加大了哈希冲突,降低hashmap的性能。
5.哈希表底层采用何种算法计算hash值?还有哪些算法可以计算出哈希值?
(1)底层采用的是key的hashCode方法的值结合数组长度进行(移位运算)无符号右移(>>>),按位异或(^),按位与(&)计算出索引。
(2)还可以采用:平方取中法,取余数,伪随机数法
5.hashmap存储元素的过程
在创建一个map对象后,调用put方法,传入key值及value值,首先将key值进行hash运算得到的hash值作为该entry键值对在数组中的索引位置。
此时确定该数组位置后,首先判断该位置是否为null。如果为null,则将entry存储在该位置;如果不为null,将entry以链表的方式存储在数组中,此时,通过equals()方法判断该位置的元素的key值与要存入的entry键值对的key值比较是否相等,若相等,覆盖value值存入,若不相等,则直接存入。
put方法存值的具体步骤:
1)先通过hash值计算出key映射到哪个桶;
2)如果桶上没有发生碰撞冲突,则直接插入;
3)如果出现碰撞冲突了,则需要处理冲突:
a:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
b:否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
4)如果桶中存在重复的键,则为该键替换新值value;
5) 如果size大于阈值threshold,则进行扩容;
6.为什么转换成红黑树的节点是8
因为红黑树节点的大小是普通节点的两倍,只有当bin(桶)包含足够多的节点时才会转成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降为6就转成普通bin。
TREEIFY_THRESHOLD = 8是根据概率统计决定的,因为符合泊松分布,超过8的时候,概率已经非常小的,所以我们选择8这个数字。
//当链表的值超过8则会转红黑树(1.8新增)
static final int TREEIFY_THRESHOLD = 8;
//当链表的值小于6则会从红黑树转回链表
static final int UNTREEIFY_THRESHOLD = 6;
以上就是今天分享的内容,如有问题,欢迎指正