HashMap基本简介
HashMap是基于哈希表的Map接口实现的,是以Key-value方式存在的键值对。HashMap的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构,结构变得复杂了,但是效率也变的更高效。
HashMap的底层数据结构
在JDK1.7之前,HashMap的底层实现是通过: 数组 + 链表实现的。
在JDK1.8之后,HashMap的底层实现是通过:数组 + 链表/红黑树实现的,在引入了红黑树之后,让其结构变得更加复杂,但是极大的提高了查询效率。
-
在初始化Hash桶的时候,默认的初始化的大小:16
-
决定是否resize的负载因子:默认的负载因子:0.75
-
HashMap的属性
//默认的table的初始化大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
//table的最大
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子,决定table什么时候进行扩容操作,通常是通过
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转换成红黑树的节点个数限制条件
static final int TREEIFY_THRESHOLD = 8;
//红黑树转化成链表的节点个数
static final int UNTREEIFY_THRESHOLD = 6;
//链表转化成红黑树 table的基础条件,当table > 64 并且 链表的长度 > 8 的时候才会从链表转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap的核心方法
-
HashMap的 hash() 方法
//JDK1.7版本
static int hash(int h) {
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);
}
//例子:
10101100 10101100 10010101 11001101 //散列值
00000000 00000000 10101100 10101100 //散列值向右移动16位
-------------------------------------------------------- ^操作:两个值不相同结果为1,否则结果为0
10101100 10101100 00111001 01100001 //高16位和低16的异或操作,目的是为了增加其低位的一个随机性
//获取table中的位置
static int indexFor(int h, int length){
return h & (length - 1);//这里可以解释为什么HashMap中的table的容量为2的N次方,length- 1之后,正好相当于一个低位的掩码
}
例子:table.length = 16
10101100 10101100 00111001 01100001 //散列值
00000000 00000000 00000000 00001111 //table的二进制的数据,length - 1 = 15
---------------------------- &操作
00000000 00000000 00000000 00000001 = 1,index值,完成了高位全部为0
这里采用的是对象的hash值,采用hash值与低16位的一个异或的操作
-
HashMap的 put() 方法
-
HashMap的 resize()方法
resize的核心功能点:
- 判断是否达到扩容点,进行扩容
- 原始的数据进行重新hash,在JDK1.7之前,是全量的数据进行rehash;在JDK1.8之后,采用位置不变或则 索引 + 旧容量大小
JDK1.8之后相比JDK1.7,HashMap的优化
- 底层的数据结构的改变:由底层的
数组+链表---〉数组+链表/红黑树。底层数据结构的改变提高了查询数据的效率,由之前的链表查询的事件 O(n) 变成了O(logn)。 - 链表的插入方式:JDK1.8采用了
尾插法,JDK1.7之前使用的是头插法 - 扩容的时候:JDK1.7需要对原数组中的元素进行
重新hash定位在新数组的位置,JDK1.8中采用更简单的判断逻辑,位置不变或索引+旧容量大小 - 线程安全方面:JDK1.7在多线程环境下,扩容时会造成环形链或数据丢失。JDK1.8中,在多线程的环境下面,会发生数据覆盖的情况。
HashMap底层table总是2的n次方
- 当length = 2的n次方的时候,
h & (length-1) = h % length更加高效,因为位运算直接对内存数据进行操作,不需要转成十进制,所以位运算要比取模运算的效率更高 - 当length为2的N次方的时候,
数据分布均匀,减少冲突
HashMap线程安全方面会出现的问题
- put的时候,多线程的情况下面会出现数据不一致
- resize()引起是循环