HashMap原理分析(含1.8以后的红黑树)
一、概述
HashMap这个类不管是Java开发还是Android开发都会经常用到,当有数据需要通过键值对的形式存储的时候,使用Map会非常的方便。为什么要学习HashMap的原理呢?其中有两点原因:
1.通过对HashMap原理的学习,可以修炼开发者的内功,因为一旦理解的HashMap等于把数据结构都理解了(数组、链表、二叉树)。ps:线性表又叫数组,红黑树又叫二叉树
2.第二个原因比较有意思,因为它是大厂高频出现的一道面试题(你懂得)
二、数据结构
数据结构是一个老生常谈的问题,但是没办法,想要了解HashMap的原理这玩意根本绕不开,不要怕麻烦,一旦理解了数据结构HashMap的原理你一看就懂,而且看一遍基本上就会被了。
jdk1.8以前的HashMap的数据结构=数组(数组)+链表
jdk1.8及以后的HashMap的数据结构=数组(数组)+链表+红黑树(二叉树)
由于篇幅有限,下面简单说一下线性表、链表、红黑树的数据结构的特性。
1.数组:0个或多个数据元素的有限序列。
它是一个顺序存储结构,你可以把它理解称为一个数组。例如:存储的第一个元素在数组下标为0的位置,第二个在数组下标为1的位置依次类推。由于线性表的这个顺序存储特性,导致了其查询效率特别高,以为只需要拿到下标就可以获取到数组下标对应的元素值。但是其插入和删除的效率就不敢恭维了,由于是顺序存储导致了数组中不可能有空位置,如果再数组下标为0的位置删除一个元素,那么数组中所有的元素都需要向前移动一位,其时间复杂度为(O(n))。插入也是同样的道理,如果在数组中间位置插入一个元素,则插入位置之后的所有数组元素都需要向后移动一位,效率相对糟糕。
优点:查询效率很高
缺点:插入和删除效率低下
2.链表:用一组任意存储单元存储线性表的数据元素,这组存储单元可以是连续的也可以是不连续的。
链表也是一种线性表,这是它里面的元素可以是连续的也可以是不连续的,这就意味这不用提前开辟一块存储容量,当链表中没有元素时,不占用内存空间。数组中的元素必须是连续存储的,而链表不必这么干,我们可以在任意的内存地址中存储我们的链表元素,而这些内存地址还可以是分散的。这意味着我们在插入和删除的时候不需要做任何的移动操作。这就大大提高了插入和删除的效率。但是单链表的读取却是一个麻烦事,因为每次读取都需要从头开始查找,直到把元素找到为止,其时间复杂度为(O(n))。
优点:插入和删除效率高
缺点:查询效率低下
3.二叉树(红黑树):树结构是一种一对多的数据结构。
一棵树中有一个根节点,根节点下又有子节点和叶子节点,子节点下又有子节点和叶子节点,依次往下推构成了一颗完整的树。上面讲到线性表的有序结构(数组)和线性表的链表结构,它们两个都有其各自的优点和缺点,根据不同的使用场景来选取不同的数据结构。但是有没有一种结构是综合以上两种结构的优点的呢?还真有,它就是现在正在讲的二叉树结构。
有点:插入和删除的效力都相对较高
缺点:和数组、链表的优点相比,比较弱
废话不多说,下面分析一下HashMap的原理(纯原理)
四、HashMap原理
1.jdk1.8以前
在jdk1.8以前HashMap的数据结构是数组+链表的形式。
1.在HashMap创建的时候会初始化一个数组,这个数组的容量是16,用来存储存放进去的元素。
2.put(key,value)
HashMap在添加元素时会根据key计算计算出一个hash值,用来确定元素在数组中的存放位置。这里面有两种情况:1.如果计算出来的hash值在数组中的位置不存在元素,就直接把这个元素放入该位置,如果已经存在了元素(发生了碰撞),就以链表的形式把元素存储在这个位置,新加入的元素在链表头部,最先加入的放在链表的尾部。
3.get(key)
在数组中获取元素的时候,现根据key计算出hash值,然后利用hash计算出元素在数组中的位置,然后遍历链表利用key.equals方法取出对应的元素。
2.jdk1.8以后
在jdk1.8以及以后引入了红黑树数据结构,其HashMap的数据结构=数组+链表+红黑树
1.在HashMap创建时依然会初始化一个数组大小,其大小为16.
2.put(key,value)
根据key计算出key的hash值,如果hash值对应的位置在数组中没有对应的元素就直接存入,如果有就把新元素加入链表头,最先加入的元素放到链表尾部。
3.get(key)
根据key计算出key的hash值,然后利用hash值查数组中对应位置的链表,中的元素,这里有一个前提条件。1.如果链表的长度小于8就直接从链表中查找,并取出。2.如果链表的长度大于等于8就把链表转换为红黑树然后遍历树取出元素。
面试中最容易问到的三个问题:
1.HashMap扩容原理?
当HashMap的元素越来越多的时候其发生碰撞的几率也会越来越大(数组长度固定)。所以为了提高查询效率就需要对HashMap进行扩容,但是扩容后会对性能有较大的影响,因为数组长度扩展以后会把原来的数组链表的元素都移动到新的数组中。
HashMap的初始化容量是16,HashMap有一个加载因子(loadFactor)默认值为0.75。当HashMap中的元素个数超过HashMap容量*0.75的时候就会发生扩容。扩容后的数组大小为2*原始容量,即二倍。
举个栗子:原始容量是16,加载因子(loadFactor)是0.75。也就是说当HashMap中元素的个数>16*0.75=12的时候就会发生扩容。
2.HashMap如何解决碰撞的?
HashMap使用链表来解决碰撞冲突,当数组中的key的hash值位置上有元素是,就把这个元素放入链表的头部,最先加入的放到链表的尾部。
3.为什么要在jdk1.8以后再HashMap中引入红黑树?
引入红黑树是为了提高HashMap的查询效率。假设一种情况:当HashMap中的碰撞越来越多,链表越来越长的时候,其获取单个元素所需要的时间就会越来越高(因为链表的查询速度比较慢)。为了解决这个问题jdk1.8引入了红黑树。因为红黑树的查询速度比链表要高很多。