因为热爱所以坚持,因为热爱所以等待。熬过漫长无戏可演的日子,终于换来了人生的春天,共勉!!!
/*
* 序列化版本号
*/
private static final long serialVersionUID = 362498820763181265L;
/**
* HashMap的初始化容量(必须是 2 的 n 次幂)默认的初始容量为16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大的容量为2的30次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的装载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树化阈值,当一个桶中的元素个数大于等于8时进行树化
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 树降级为链表的阈值,当一个桶中的元素个数小于等于6时把树转化为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当桶的个数达到64的时候才进行树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* Node数组,又叫作桶(bucket)
*/
transient Node<K,V>[] table;
/**
* 作为entrySet()的缓存
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 元素的数量
*/
transient int size;
/**
* 修改次数,用于在迭代的时候执行快速失败策略
*/
transient int modCount;
/**
* 当桶的使用数量达到多少时进行扩容,threshold = capacity * loadFactor
*/
int threshold;
/**
* 装载因子
*/
final float loadFactor;
(1)容量:容量为数组的长度,亦即桶的个数,默认为16 ,最大为2的30次方,当容量达到64时才可以树化。
(2)装载因子:装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。
(3)树化:树化,当容量达到64且链表的长度大于8时进行树化,当链表的长度小于6时可能反树化。
面试问题:
// 默认的初始容量是16 1 << 4 相当于 1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
HashMap 构造方法可以指定集合的初始化容量大小,如:
// 构造一个带指定初始容量和默认负载因子(0.75)的空 HashMap。
HashMap(int initialCapacity)
根据上述讲解我们已经知道,当向 HashMap 中添加一个元素的时候,需要根据 key 的 hash 值,去确定其在数组中的具体位置。HashMap 为了存取高效,减少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现的关键就在把数据存到哪个链表中的算法。
这个算法实际就是取模,hash % length,而计算机中直接求余效率不如位移运算。所以源码中做了优化,使用 hash & (length - 1),而实际上 hash % length 等于 hash & ( length - 1) 的前提是 length 是 2 的 n 次幂
。
例如,数组长度为 8 的时候,3 & (8 - 1) = 3,2 & (8 - 1) = 2,桶的位置是(数组索引)3和2,不同位置上,不碰撞。
从上图可以看出,当数组长度为9(非2 的n次幂)的时候,不同的哈希值hash, hash & (length - 1) 所得到的数组下标相等(很容易出现哈希碰撞)。
HashMap
HashMap双参构造函数会通过tableSizeFor(initialCapacity)方法,得到一个最接近length且大于length的2的n次幂数(比如最接近10且大于10的2的n次幂数是16)
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
说明:
当在实例化 HashMap 实例时,如果给定了 initialCapacity,由于 HashMap 的 capacity 必须是 2 的幂,因此这个方法tableSizeFor(initialCapacity);用于找到大于等于 initialCapacity 的最小的 2 的幂。
分析:
1.int n = cap - 1;为什么要减去1呢?
防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂,又没有这个减 1 操作,则执行完后面的几条无符号操作之后,返回的 capacity 将是这个 cap 的 2 倍(后面还会再举个例子讲这个)。
2.最后为什么有个 n + 1 的操作呢?
如果 n 这时为 0 了(经过了cap - 1后),则经过后面的几次无符号右移依然是 0,返回0是肯定不行的,所以最后返回n+1最终得到的 capacity 是1。
3.注意:容量最大也就是 32bit 的正数,因此最后 n |= n >>> 16;最多也就 32 个 1(但是这已经是负数了,在执行 tableSizeFor 之前,对 initialCapacity 做了判断,如果大于MAXIMUM_CAPACITY(2 ^ 30),则取 MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY,会执行位移操作。所以这里面的位移操作之后,最大 30 个 1,不会大于等于 MAXIMUM_CAPACITY。30 个 1,加 1 后得 2 ^ 30)。
所以由结果可得,当执行完tableSizeFor(initialCapacity);方法后,得到的新capacity是最接近initialCapacity且大于initialCapacity的2的n次幂的数。
HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。
jdk1.8 之前 HashMap 由 数组 + 链表 组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。
HashMap 默认初始桶位数16,如果某个桶中的链表长度大于8,则先进行判断:
如果桶位数小于64,则先进行扩容(2倍),扩容之后重新计算哈希值,这样桶中的链表长度就变短了。【定位桶的方式:通过数组下标 i 定位,添加元素时,目标桶位置 i 的计算公式,i = hash & (cap - 1),cap为容量】
如果桶位数大于等于64,且某个桶中的链表长度大于8,则对链表进行树化(红黑树,即自平衡的二叉树)
如果红黑树的节点数在小于等于6时,红黑树可能会重新变会链表。
我们来分析分析上面这个逻辑,进入这个untreeify() 的要求是,root == null, root.right null, root.leftnull, root.left.left==null四种情况,我们以7个节点的红黑树来分析,A为root节点。
1.最多节点情况:当我们删除节点D时,满足root.left.left==null这个条件,此时节点数为6,这棵树要进行非树化;而如果选择删除G节点,这时节点数也为6,但是不用退树化,最大节点数为6。
2.最少节点情况:当EFG不存在时,在A,B,C,D中删除任意一个节点,都会满足上述四种规则中的一种。则存在最少节点情况,有4个节点,此时不会树化。
节点数量原因分析
为什么在小于6的时候可能转换成链表,而在大于8的时候转化成红黑树?
主要通过时间查询节点分析,红黑树的平均查询时间为 log(n), 而链表是O(n),平均是O(n)/2。
原因:
首先,HashMap
当向哈希表中存储put(“a”, 3) 的数据时,根据"a"字符串调用 String 类中重写之后的 hashCode() 方法计算出哈希值,然后结合数组长度(桶数量)采用某种算法计算出向 Node 数组中存储数据的空间索引值(比如table[i],这里的i就是该Node数组的空间索引)。如果计算出的索引空间没有数据(即,这个桶是空的),则直接将<“a”, 3>存储到数组中。
当向哈希表中存储数据<“b”, 4>时,假设算出的 hashCode() 方法结合数祖长度计算出的索引值也是3,那么此时数组空间不是 null(即,这个桶目前不为空),此时底层会比较 "a"和 “b” 的 hash 值是否一致,如果不一致,则在空间上划出一个结点来存储键值对数据对 <“b”, 4>,这种方式称为拉链法。
当向哈希表中存储数据 <“a”, 88888> 时,那么首先根据 "a"调用 hashCode() 方法结合数组长度计算出索引肯定是 3,此时比较后存储的数据"a"和已经存在的数据的 hash 值是否相等,如果 hash 值相等,此时发生哈希碰撞。那么底层会调用 "a"所属类 String 中的 equals() 方法比较两个内容是否相等:
相等:将后添加的数据的 value 覆盖之前的 value。
不相等:继续向下和其他的数据的 key 进行比较,如果都不相等,则划出一个结点存储数据,如果结点长度即链表长度大于阈值 8 并且数组长度大于 64 则将链表变为红黑树。
综上描述,当位于一个表中的元素较多,即 hash 值相等但是内容不相等的元素较多时,通过 key 值依次查找的效率较低。而 jdk1.8 中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阈值)超过8且当前数组的长度大于64时,将链表转换为红黑树,这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。如下图所示:
jdk1.8 中引入红黑树的进一步原因:
jdk1.8 以前 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。
针对这种情况,jdk1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。
capacity(容量)* loadFactor(负载因子)
。这个值是当前已占用数组长度的最大值。size 超过这个值就重新 resize(扩容),扩容后的 HashMap 容量是之前容量的2倍。1.resize扩容优化
2.引入了红黑树
3.解决了多线程死循环问题,但仍然是非线程安全
差异 | JDK1.7 | JDK1.8 |
---|---|---|
存储结构 | 数组+链表 | 数据+链表+红黑树 |
初始化方式 | 单独函数:inflateTable() | 直接集成到了resize()中 |
hash值计算方式 | 9次扰动 = 4次位运算 + 5次异或运算 | 2次扰动=1次位运算+一次异或 |
存放数据的规则 | 无冲突时,存放在数组上,有冲突时,用拉链法形成一条链表,头结点在数组上 | 无冲突时,存放在数组上,有冲突时,如果数组长度小于64,先扩容;如果数组长度大于等于64且链表的长度大于8,将该链表转化为红黑树结构 |
插入数据的方式 | 头插法 | 尾插法(在链表/红黑树尾部插入) |
扩容后存储位置的计算 | 遍历全部元素重新hash计算位置 | 扩容后:1.如果是单个元素,重新hash运算一次;2.旧元素 e.hash & oldCap = 0,新表中与旧表中位置一样;3.旧元素 e.hash & oldCap != 0, 位置为旧表位置+旧表容量 |
1.线程是否安全方面
HashMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过synchronized修饰。(如果要保证线程安全的话就使用ConcurrentHashMap吧!)
2.效率方面
HashMap要比HashTable(使用synchronized加锁)效率高。另外,HashTable基本被淘汰,请不要在代码中使用它
3.对key为null或者value为null的支持方面
HashMap支持一个key为null,当key为null时,直接hash方法直接返回零值,这样的键只可以有一个,value可以有一个或多个;hashTable中key-value都不能为null,如果为null会抛出空指针异常(NullPointerException)
4.原始容量大小与每次扩充容量大小的不同方面
①创建时如果不指定容量初始值, Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1.;HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
②创建时如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证)。也就是说HashMap总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
5.底层数据结构方面
JDK1.8以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。