JDK源码学习-HashMap

167 阅读4分钟

HashMap基本简介

HashMap是基于哈希表的Map接口实现的,是以Key-value方式存在的键值对。HashMap的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构,结构变得复杂了,但是效率也变的更高效。

HashMap的底层数据结构

在JDK1.7之前,HashMap的底层实现是通过: 数组 + 链表实现的。 在JDK1.8之后,HashMap的底层实现是通过:数组 + 链表/红黑树实现的,在引入了红黑树之后,让其结构变得更加复杂,但是极大的提高了查询效率。

HashMap.png

  • 在初始化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() 方法

20200618150149962.png

  • HashMap的 resize()方法

822073-20200429111754905-694722259.png 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()引起是循环