做者:小傅哥
博客:https://bugstack.cnhtml
沉淀、分享、成长,让本身和他人都能有所收获!😄
得益于Doug Lea
老爷子的操刀,让HashMap
成为使用和面试最频繁的API,没办法设计的太优秀了!java
HashMap 最先出如今 JDK 1.2中,底层基于散列算法实现。HashMap 容许 null 键和 null 值,在计算哈键的哈希值时,null 键哈希值为 0。HashMap 并不保证键值对的顺序,这意味着在进行某些操做后,键值对的顺序可能会发生变化。另外,须要注意的是,HashMap 是非线程安全类,在多线程环境下可能会存在问题。程序员
HashMap 最先在JDK 1.2中就出现了,底层是基于散列算法实现,随着几代的优化更新到目前为止它的源码部分已经比较复杂,涉及的知识点也很是多,在JDK 1.8中包括;一、散列表实现
、二、扰动函数
、三、初始化容量
、四、负载因子
、五、扩容元素拆分
、六、链表树化
、七、红黑树
、八、插入
、九、查找
、十、删除
、十一、遍历
、十二、分段锁
等等,因涉及的知识点较多因此须要分开讲解,本章节咱们会先把目光放在前五项上,也就是关于数据结构的使用上。面试
数据结构相关每每与数学离不开,学习过程当中建议下载相应源码进行实验验证,可能这个过程有点烧脑,但学会后不用死记硬背就能够理解这部分知识。算法
本章节涉及的源码和资源在工程,interview-04中,包括;数组
interview-04
工程中能够经过关注公众号:bugstack虫洞栈
,回复下载进行获取{回复下载后打开得到的连接,找到编号ID:19}安全
学习HashMap前,最好的方式是先了解这是一种怎么样的数据结构来存放数据。而HashMap通过多个版本的迭代后,乍一看代码仍是很复杂的。就像你原来只穿个裤衩,如今还有秋裤和风衣。因此咱们先来看看最根本的HashMap是什么样,也就是只穿裤衩是什么效果,以后再去分析它的源码。数据结构
问题: 假设咱们有一组7个字符串,须要存放到数组中,但要求在获取每一个元素的时候时间复杂度是O(1)。也就是说你不能经过循环遍历的方式进行获取,而是要定位到数组ID直接获取相应的元素。多线程
方案: 若是说咱们须要经过ID从数组中获取元素,那么就须要把每一个字符串都计算出一个在数组中的位置ID。字符串获取ID你能想到什么方式? 一个字符串最直接的获取跟数字相关的信息就是HashCode,可HashCode的取值范围太大了[-2147483648, 2147483647]
,不可能直接使用。那么就须要使用HashCode与数组长度作与运算,获得一个能够在数组中出现的位置。若是说有两个元素获得一样的ID,那么这个数组ID下就存放两个字符串。函数
以上呢其实就是咱们要把字符串散列到数组中的一个基本思路,接下来咱们就把这个思路用代码实现出来。
// 初始化一组字符串 List<String> list = new ArrayList<>(); list.add("jlkk"); list.add("lopi"); list.add("小傅哥"); list.add("e4we"); list.add("alpo"); list.add("yhjk"); list.add("plop"); // 定义要存放的数组 String[] tab = new String[8]; // 循环存放 for (String key : list) { int idx = key.hashCode() & (tab.length - 1); // 计算索引位置 System.out.println(String.format("key值=%s Idx=%d", key, idx)); if (null == tab[idx]) { tab[idx] = key; continue; } tab[idx] = tab[idx] + "->" + key; } // 输出测试结果 System.out.println(JSON.toJSONString(tab));
这段代码总体看起来也是很是简单,并无什么复杂度,主要包括如下内容;
0111
除高位之外都是1的特征,也是为了散列。key.hashCode() & (tab.length - 1)
。模拟链表的过程
。测试结果
key值=jlkk Idx=2 key值=lopi Idx=4 key值=小傅哥 Idx=7 key值=e4we Idx=5 key值=alpo Idx=2 key值=yhjk Idx=0 key值=plop Idx=5 测试结果:["yhjk",null,"jlkk->alpo",null,"lopi","e4we->plop",null,"小傅哥"]
e4we->plop
。若是上面的测试结果不能在你的头脑中很好的创建出一个数据结构,那么能够看如下这张散列示意图,方便理解;
以上咱们实现了一个简单的HashMap,或者说还算不上HashMap,只能算作一个散列数据存放的雏形。但这样的一个数据结构放在实际使用中,会有哪些问题呢?
以上这些问题能够概括为;扰动函数
、初始化容量
、负载因子
、扩容方法
以及链表和红黑树
转换的使用等。接下来咱们会逐个问题进行分析。
在HashMap存放元素时候有这样一段代码来处理哈希值,这是java 8
的散列值扰动函数,用于优化散列效果;
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
理论上来讲字符串的hashCode
是一个int类型值,那能够直接做为数组下标了,且不会出现碰撞。可是这个hashCode
的取值范围是[-2147483648, 2147483647],有将近40亿的长度,谁也不能把数组初始化的这么大,内存也是放不下的。
咱们默认初始化的Map大小是16个长度 DEFAULT_INITIAL_CAPACITY = 1 << 4
,因此获取的Hash值并不能直接做为下标使用,须要与数组长度进行取模运算获得一个下标值,也就是咱们上面作的散列列子。
那么,hashMap源码这里不仅是直接获取哈希值,还进行了一次扰动计算,(h = key.hashCode()) ^ (h >>> 16)
。把哈希值右移16位,也就正好是本身长度的一半,以后与原哈希值作异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。计算方式以下图;
从上面的分析能够看出,扰动函数使用了哈希值的高半区和低半区作异或,混合原始哈希码的高位和低位,以此来加大低位区的随机性。
但看不到实验数据的话,这终究是一段理论,具体这段哈希值真的被增长了随机性没有,并不知道。因此这里咱们要作一个实验,这个实验是这样作;
扰动函数对比方法
public class Disturb { public static int disturbHashIdx(String key, int size) { return (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16)); } public static int hashIdx(String key, int size) { return (size - 1) & key.hashCode(); } }
disturbHashIdx
扰动函数下,下标值计算hashIdx
非扰动函数下,下标值计算单元测试
// 10万单词已经初始化到words中 @Test public void test_disturb() { Map<Integer, Integer> map = new HashMap<>(16); for (String word : words) { // 使用扰动函数 int idx = Disturb.disturbHashIdx(word, 128); // 不使用扰动函数 // int idx = Disturb.hashIdx(word, 128); if (map.containsKey(idx)) { Integer integer = map.get(idx); map.put(idx, ++integer); } else { map.put(idx, 1); } } System.out.println(map.values()); }
以上分别统计两种函数下的下标值分配,最终将统计结果放到excel中生成图表。
以上的两张图,分别是没有使用扰动函数和使用扰动函数的,下标分配。实验数据;
未使用扰动函数
使用扰动函数
接下来咱们讨论下一个问题,从咱们模仿HashMap的例子中以及HashMap默认的初始化大小里,均可以知道,散列数组须要一个2的倍数的长度,由于只有2的倍数在减1的时候,才会出现01111
这样的值。
那么这里就有一个问题,咱们在初始化HashMap的时候,若是传一个17个的值new HashMap<>(17);
,它会怎么处理呢?
在HashMap的初始化中,有这样一段方法;
public HashMap(int initialCapacity, float loadFactor) { ... this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
threshold
,经过方法tableSizeFor
进行计算,是根据初始化来计算的。计算阀值大小的方法;
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; }
那这里咱们把17这样一个初始化计算阀值的过程,用图展现出来,方便理解;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
负载因子是作什么的?
负载因子,能够理解成一辆车可承重重量超过某个阀值时,把货放到新的车上。
那么在HashMap中,负载因子决定了数据量多少了之后进行扩容。这里要提到上面作的HashMap例子,咱们准备了7个元素,可是最后还有3个位置空余,2个位置存放了2个元素。 因此可能即便你数据比数组容量大时也是不必定能正正好好的把数组占满的,而是在某些小标位置出现了大量的碰撞,只能在同一个位置用链表存放,那么这样就失去了Map数组的性能。
因此,要选择一个合理的大小下进行扩容,默认值0.75就是说当阀值容量占了3/4s时赶忙扩容,减小Hash碰撞。
同时0.75是一个默认构造值,在建立HashMap也能够调整,好比你但愿用更多的空间换取时间,能够把负载因子调的更小一些,减小碰撞。
为何扩容,由于数组长度不足了。那扩容最直接的问题,就是须要把元素拆分到新的数组中。拆分元素的过程当中,原jdk1.7中会须要从新计算哈希值,可是到jdk1.8中已经进行优化,不在须要从新计算,提高了拆分的性能,设计的仍是很是巧妙的。
@Test public void test_hashMap() { List<String> list = new ArrayList<>(); list.add("jlkk"); list.add("lopi"); list.add("jmdw"); list.add("e4we"); list.add("io98"); list.add("nmhg"); list.add("vfg6"); list.add("gfrt"); list.add("alpo"); list.add("vfbh"); list.add("bnhj"); list.add("zuio"); list.add("iu8e"); list.add("yhjk"); list.add("plop"); list.add("dd0p"); for (String key : list) { int hash = key.hashCode() ^ (key.hashCode() >>> 16); System.out.println("字符串:" + key + " \tIdx(16):" + ((16 - 1) & hash) + " \tBit值:" + Integer.toBinaryString(hash) + " - " + Integer.toBinaryString(hash & 16) + " \t\tIdx(32):" + (( System.out.println(Integer.toBinaryString(key.hashCode()) +" "+ Integer.toBinaryString(hash) + " " + Integer.toBinaryString((32 - 1) & hash)); } }
测试结果
字符串:jlkk Idx(16):3 Bit值:1100011101001000010011 - 10000 Idx(32):19 1100011101001000100010 1100011101001000010011 10011 字符串:lopi Idx(16):14 Bit值:1100101100011010001110 - 0 Idx(32):14 1100101100011010111100 1100101100011010001110 1110 字符串:jmdw Idx(16):7 Bit值:1100011101010100100111 - 0 Idx(32):7 1100011101010100010110 1100011101010100100111 111 字符串:e4we Idx(16):3 Bit值:1011101011101101010011 - 10000 Idx(32):19 1011101011101101111101 1011101011101101010011 10011 字符串:io98 Idx(16):4 Bit值:1100010110001011110100 - 10000 Idx(32):20 1100010110001011000101 1100010110001011110100 10100 字符串:nmhg Idx(16):13 Bit值:1100111010011011001101 - 0 Idx(32):13 1100111010011011111110 1100111010011011001101 1101 字符串:vfg6 Idx(16):8 Bit值:1101110010111101101000 - 0 Idx(32):8 1101110010111101011111 1101110010111101101000 1000 字符串:gfrt Idx(16):1 Bit值:1100000101111101010001 - 10000 Idx(32):17 1100000101111101100001 1100000101111101010001 10001 字符串:alpo Idx(16):7 Bit值:1011011011101101000111 - 0 Idx(32):7 1011011011101101101010 1011011011101101000111 111 字符串:vfbh Idx(16):1 Bit值:1101110010111011000001 - 0 Idx(32):1 1101110010111011110110 1101110010111011000001 1 字符串:bnhj Idx(16):0 Bit值:1011100011011001100000 - 0 Idx(32):0 1011100011011001001110 1011100011011001100000 0 字符串:zuio Idx(16):8 Bit值:1110010011100110011000 - 10000 Idx(32):24 1110010011100110100001 1110010011100110011000 11000 字符串:iu8e Idx(16):8 Bit值:1100010111100101101000 - 0 Idx(32):8 1100010111100101011001 1100010111100101101000 1000 字符串:yhjk Idx(16):8 Bit值:1110001001010010101000 - 0 Idx(32):8 1110001001010010010000 1110001001010010101000 1000 字符串:plop Idx(16):9 Bit值:1101001000110011101001 - 0 Idx(32):9 1101001000110011011101 1101001000110011101001 1001 字符串:dd0p Idx(16):14 Bit值:1011101111001011101110 - 0 Idx(32):14 1011101111001011000000 1011101111001011101110 1110
zuio
因计算结果 hash & oldCap
不为1,则被迁移到下标位置24。一、散列表实现
、二、扰动函数
、三、初始化容量
、四、负载因子
、五、扩容元素拆分
。