HashMap
简介
HashMap 的基础知识
-
HashMap 是由数组+链表+红黑树实现的,
-
数组长度固定,一个元素进来,先计算其 hash值,然后 hash值 & 数组长度,得到的值就是该元素存放在数组上的下标位置,然后放入链表,
-
假如链表长度大于8,就从数组转变成红黑树。假如此时整个数组的链表节点大于64,创建新链表的时候就会默认创建红黑树。红黑树可以加快搜索的效率。
-
HashMap 的主干是一个 Entry数组。Entry 是 HashMap 的基本组成单元,每一个 Entry包含一个 key-value 键值对,
-
HashMap 有 4个构造器,
-
public HashMap(int initialCapacity, float loadFactor)
-
public HashMap(int initialCapacity)
-
public HashMap()
-
public HashMap(Map<? extends K, ? extends V> m)
-
-
如果用户初始化的时候没有传入 initialCapacity(初始化容量) 和 loadFactor(负载系数,负载因子) 这两个参数,会使用默认值,initialCapacity 默认为16,loadFactory默认为0.75f。initialCapacity 最大值是 1<<30=2^(31-1)≈10亿多。阈值 threshold 的 最大值为 0x7fffffff,即 int 的最大值。
-
其中负载因子是对 HashMap 空间和时间效率的一个平衡, 因为如果跟 ArrayList 一样等到放不下再进行扩容的时候,某一个 Hash的Key所对应的Value链表可能已经很长了。
HashMap 的添加步骤
-
在第一次 put 之前,是没有 table 的,table 会在第一次 put 的时候被初始化。
-
阈值一开始就是 容量加载因子。如果是无参构造,那么就是初始容量 16,初始阈值 12;如果是有参构造,那么就会调用 tableSizeFor方法得到大于输入值的最小的 2 的幂次值,然后把这个值先赋值给阈值,然后让容量等于阈值,然后阈值等于容量加载因子。并且之后每次扩容,table 和阈值都变为原来的两倍。
-
树化的条件,第一个先决条件是哈希表容量大于等于 64,第二个是一个桶里面的节点个数大等于 8。反树化条件就是小等于 6(8*加载因子)的时候。
-
扩容之后,旧 key,要么就是呆在老地方,要么就是原位置值 + 旧容量。位置计算公式是 (n-1)& hash,因为每次扩容都是增加一个 bit位,然后 key 通过 hash函数得到的 hash值是不变的,只是 n-1 在变。
-
扩容时机: 第一:元素个数达到阈值。
第二:hashmap 准备树化但发现数组太短。
hashmap 解决 hash冲突的方法就是:链地址法。
0.75 则是通过概率分布计算得到的,好像是 泊松分布。主要目的就是为了在 hash冲突和空间利用之中找到平衡点。
不增大容量,就会导致元素堆积,并且 hash冲突较多;
增大容量,就是降低空间利用率,但是减少 hash冲突,并且使的平均元素分布,不让一个桶的元素过多;
-
建 HashMap, 初始容量为 16, 阈值 = 初始容量*负载因子 (默认 0.75)= 12;
-
调用 put方法,会先计算key 的 hash 值:hash = key.hashCode()。
-
调用 tableSizeFor()方法,保证哈希表散列均匀。
-
计算 Nodes[index]的索引:先进行 index = (tab.length - 1) & hash。
-
如果索引位为 null,直接创建新节点,如果不为 null,再判断所因为上是否有元素
-
如果有:则先调用hash()方法判断,再调用 equals()方法进行判断,如果都相同则直接用新的 Value 覆盖旧的;
-
如果不同,再判断第一个节点类型是否为树节点(涉及到:链表转换成树的阈值,默认 8),如果是,则按照红黑树的算法进行存储;如果不是,则按照链表存储;
-
当存储元素过多时,需要进行扩容:默认的负载因子是 0.75,如果实际元素所占容量占分约变为原来的2 倍(newThr = oldThr << 1);
HashMap 中的常量
-
DEFAULT_INITIAL_CAPACITY:默认初始化容量,可以存储元素(Entry)的数量,值为16;
-
MAXIMUM_CAPACITY:最大容量,允许设置的最大存储数量,值为2^30;
-
DEFAULT_LOAD_FACTOR:默认负载因子,用于控制扩容的阈值,值为0.75;
-
TREEIFY_THRESHOLD:链表结构转为红黑树树的阈值,为8,即当链表结构长度达到8时,进行结构转化;
-
UNTREEIFY_THRESHOLD:树结构转为链表的阈值,为6,即当树的节点数达到6时,转化为链表;
-
MIN_TREEIFY_CAPACITY:最小树形化容量阈值,为64,即当HashMap中的元素总数大于64时,才允许将链表转换成红黑树,否则,若桶内元素太多时,则直接扩容,而不是树形化
对 threshold 的理解
问题1
一个 map,负载因子是 0.75,初始化时设定的大小是 10,会被分配16的空间,那么 map容量是在 100.75 还是 160.75 时扩容?
解答
hashmap 在扩容的时候会对 threshold 进行判断,size 大于等于 threshold 的时候就扩容。
然后这个 threshold 就是 2 的次方产生的结果数乘上加载因子,threshold 取的值,是你设定的大小的下一个 2 的次方数,比如 10 的下一个 2 的次方数是 16,所以会在 16*0.75 的时候扩容。
源码理解
tableSizeFor(int cap)
this.threshold = tableSizeFor(initialCapacity); 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; }
分析
这个方法的的意义就是找到比输入值大的,并且与输入相邻的 2的次方的结果数。
因为右移做或运算,那就是 1 都移到右边去,然后 int 最高 32位,所以只需要右移 1,2,4,8,16 就行。
比如输入 3,输出 4。输入 5、6、7,输出 8。以输入 10 为例。
int n = 10-1 = 9;// 1001 n = 1001|0100 = 1101 = 13;//>>>1 n = 1101|0011 = 1111 = 15;//>>>2 n = 1111|0000 = 1111 = 15;//>>>4 n = 1111|0000 = 1111 = 15;//>>>8 n = 1111|0000 = 1111 = 15;//>>>16 return n+1=16;