1.考考你
这是咱们高级并发编程系列的第十五篇,这一篇原来是准备写ConcurrentHashMap,标题我都想好了,叫作:一文搞懂ConcurrentHashMap。java
可是想了想,要说清楚ConcurrentHashMap,还须要优先说清楚HashMap,因而咱们临时把标题换一换,换成一文搞懂HashMap。不过你放心,关于ConcurrentHashMap,我会放到下一篇来分享。编程
相信这两篇文章,你都会有所收获。我打算从分析底层原理,结合源码的方式,争取让咱们在实际项目开发中,用到HashMap,或者ConcurrentHashMap的时候,都可以知其然,还知其因此然。那么让咱们开始吧数组
#考考你: 1.不少时候,你都会直接用到HashMap,那么你真的认识HashMap吗 2.关于HashMap,你知道什么是hash冲突,负载因子,以及如何扩容吗
2.案例
2.1.HashMap原理
2.1.1.原理分析
提及HashMap的底层实现原理,用到的数据结构,你必定很熟系:数组。咱们一块儿来尝试分析一下,为何HashMap的底层数据结构会选择数组呢?缓存
你能够先回顾一下,数组这种数据结构都有什么特色。咱们一块儿来回忆一下:数组是一种基于线性表的数据结构,支持按照下标随机访问,时间复杂度是O(1),很是高效。bash
你再回顾一下,日常在项目中使用HashMap,相应的业务场景有哪些?一般状况下,咱们把HashMap做为一个容器,好比说本地缓存解决方案,把缓存目标对象,按照key/value的方式存放到HashMap中,须要用到缓存对象的时候,根据缓存key从容器中查找目标对象。数据结构
这里你须要注意两个字:查找。等价于说咱们使用HashMap,大多数业务场景都是在读多写少的场景(一次写入,屡次读取)。对于读咱们的指望是要高效,最好是O(1)的时间复杂度,嗯这不数组自然就知足吗?并发
到这里,我相信你应该可以理解了:为何HashMap的底层数据结构,会选择数组。我给你看一个对应关系图,就不会那么抽象了:函数
2.1.2.一图胜千言,图解
上图便是HashMap的底层实现,你须要关注:左边的散列函数、中间的数组、右边的链表。咱们来逐个分析一下。源码分析
咱们已经明确了HashMap的底层数据结构是数组,即HashMap中的元素,其实就是存在数组中,所以中间的数组,对于你来讲,理解起来不是什么难事。spa
这里有一个问题,咱们知道数组是按照下标来查找数组中元素的,即下标0,1,2......好比array[0]=小明。实际使用HashMap中,咱们是经过key/value键值对的方式,与之对应array[0]是HashMap中key部分,小明是value部分。
关键问题是,HashMap中key能够是任意类型,咱们须要一种方式,将任意类型的key,与数组联系起来,准确说是与数组的下标联系起来,即:任意类型key--->转换--->数组下标。这里的转换,即转换函数,就是上图中左边的散列函数hash(key)。这么解释之后,相信左边散列函数的做用,你能够理解了。
最后,右边的链表究竟是什么用意呢?它是解决散列冲突的方法之一拉链法,还有另一个解决散列冲突的方法开放寻址法。这里我先不解释关于拉链法,与开放寻址法的区别。咱们重点关注散列(hash)冲突的问题,为何就冲突了呢?
咱们知道,要把数据放入HashMap容器中,必然有一个过程,即把任意类型的key,转换成数组下标的过程。咱们知道容器是已知,且容量是有限的,好比说只能放10个元素的容器;可是要放入容器的元素是未知,无限的。将未知无限的空间,转换到已知有限的空间,必然会有冲突存在。这段话很抽象,你估计在怀疑这是人话吗?
我举个例子,你就明白了:1+4=5,2+3=5,0+5=5,你看等号左边不一样,可是右边结果都是5。也就是说hash(key):hash(1+4),hash(2+3),hash(0+5),不一样的key,通过hash函数后,会有相同值的状况出现,这就是hash冲突的由来。你看能够理解了吧。
那既然hash冲突几率上必定会发生,发生几率的大小,取决于hash函数的设计,好的hash函数设计是一件挺有难度的事情,这是另一个话题,咱们暂时不关心。咱们将重点放在发生冲突之后怎么处理?这就是我须要给你解释的上图中右边部分的链表解决的问题,若是发生了hash冲突,把冲突的元素经过链表串起来。
图解这部份内容,多少会有些抽象,你须要结合图一块儿多看一下,一旦看明白了其实也不难。我简单总结一下关于HashMap:
-
左边散列函数:用于将任意类型的key,转换成数组的下标,与数组联系起来
-
中间数组:HashMap的底层数据结构,HashMap中的元素其实是存储在数组中
-
右边链表:由于HashMap中有发生hash冲突,链表用于将冲突的多个元素串连起来
2.2.源码与差别化分析
2.2.1.关键源码分析
这里我把HashMap的关键源码列举出来,只能起到抛砖引玉的效果,我建议你实际打开完整的源码看一看会更好
/* *HashMap底层数据结构:数组 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /* *HashMap默认初始容量:16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /* *HashMap默认负载因子:0.75f */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /* *HashMap每次扩容:都是原容量的2倍 */ // 扩容调用 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { // 在原有容量上,扩容2倍 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } // 实际扩容方法 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
2.2.2.差别分析:jdk8与jdk7
关于HashMap差别化分析,实现细节上的差别分析,有两个纬度:
-
针对不一样的jdk版本(主要是jdk8,与jdk8之前)
-
差别内容:hash冲突解决方式
前面咱们分析了,HashMap中若是发生hash冲突后,经过链表(拉链法)将冲突的多个元素串联起来,解决hash冲突。你须要注意,这是jdk7及之前版本的解决方案。
在jdk8中,除了原有的拉链法,即链表方式解决hash冲突外,还引入了红黑树的解决方案。即当某个点冲突元素个数小于8的时候,仍是经过链表解决hash冲突;当冲突元素个数大于等于8之后,将链表转换成红黑树解决hash冲突。
文字描述始终比较抽象,咱们经过分享两个图直观看一下,方便你理解:
jdk7拉链法解决hash冲突:
jdk8拉链法、红黑树解决hash冲突: