【Java源码】HashMap

1、什么是哈希函数?

哈希函数

        把任意长度的输入(又叫做预映射, pre-image),通过哈希算法,变换成固定长度的输出,该输出就是哈希值。这种转换是一种压缩映射。也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。

        简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

        哈希函数每次在相同或相等的对象上应用哈希函数时, 应每次返回相同的哈希码。换句话说, 两个相等的对象必须一致地生成相同的哈希码。

Java中的Hash函数

        Java 中所有的对象都有 Hash 方法。

        Java中的所有对象都继承 Object 类中定义的 hashCode() 函数的默认实现。 此函数通常通过将对象的内部地址转换为整数来生成哈希码,从而为所有不同的对象生成不同的哈希码。

哈希冲突:

        键(key)经过hash函数得到的结果作为地址去存放当前的键值对(key-value)(这个是hashmap的存值方式),但是却发现该地址已经有人先来了,一山不容二虎,就会产生冲突。这个冲突就是hash冲突了。

        一句话说就是:如果两个不同对象的hashCode相同,这种现象称为hash冲突。

解决哈希函数的方法:

  1. 开发定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
  2. 再哈希法:这种方法是同时构造多个不同的哈希函数: Hi=RH1(key) i=1,2,…,k 。当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间
  3. 链地址法:将所有哈希地址相同的都链接在同一个链表中 ,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。 (hashmap就是用此方法解决冲突的。)

hashmap就是用此方法解决冲突的

  1. 建立一个公共溢出区 
    java中hashmap采用的是链地址法

2、HashMap的内部存储结构:

Node数组+链表或红黑树

Node数组:

主要包含几个成员:

3、数组的容量是2的整数次方的原因;

        要使HashMap存取高效,尽量减少碰撞,就要把数字尽量的分配均匀。太长的数组直接用内存存是放不下的,这个散列值不能直接拿来用,需要对数组的长度进行取模运算,求得的结果就是存放的位置。在数组长度n是2的整数次幂时,hash对数组长度n取余所得的值,与hash和(数组长度-1)求与得到的结果是等价的。

当n是2的整数次幂的时候,hash&(n-1)==hash%n。

4、get和put方法流程

        get方法:进行链表或者红黑树遍历搜索指定key的节点的过程。

        1) 通过hash(Object key)方法计算key的哈希值

        2) 通过getNode(int hash, Object key)方法获取node 

        3)如果node为null,就返回null,否则就返回node的值(node.val)

        put方法:计算每个key的hash值,通过一定计算后找到它在哈希表中的位置,将健值放到这个位置,有hash碰撞再解决hash碰撞。

5、resize方法(动态扩容)

        数组在初始化时就需要指定长度。在实际使用过程中, 设定一个阈值参数(threshold), 在存储的容量达到指定的阈值时, 需要进行扩容处理。

        1)扩容发生的条件:

            首先,初始化table数组的时候会执行resize函数。此时,数值为空或者数组长度为 0。创建HashMap的时候没有指定容量,那么table数组的初始容量是默认值:16。

            其次,当元素的数量大于阈值的时候,即size的值>threshold的时候会触发扩容。此时,table数组的大小会翻倍。

        2)容量为2的整数次幂和数组索引计算:(HashMap的容量为什么一定得是2的整数次幂呢?)

            首先,数组索引最直观的的计算方式是:hash%n,即通过取余的方式把当前的key、value键值对散列到各个桶中。但是是实际上采用的是index = hash & (n-1)这种计算方法。原因是CPU对位运算支持较好,即位运算速度很快。另外,当n是2的整数次幂时:hash&(n-1)与hash%(n-1)是等价的,但是两者效率来讲是不同的,位运算的效率远高于%运算。

            其次,将旧数组中的Node迁移到扩容后的新数组中的时候比较方便。HashMap使用table数组保存Node节点,所以table数组扩容的时候,数组扩容先重新开辟一个数组,然后再把就数组中的元素重新散列(rehash)到新数组中去。

6、影响HashMap的性能因素(key的hashCode函数实现、loadFactor、初始容量)

7、HashMap key的hash值计算方法以及原因(见上面hash函数的分析)

8、table[i]位置的链表什么时候会转变成红黑树(上面源码中有讲)

9、HashMap主要成员属性:threshold、loadFactor、HashMap的懒加载

10、HashMap的get方法能否判断某个元素是否在map中

11、HashMap线程安全吗,哪些环节最有可能出问题,为什么?

        HashMap的put操作是不安全的,因为没有使用任何锁;HashMap在多线程下最大的安全隐患发生在扩容的时候,想想一个场合:HashMap使用默认容量16,这时100个线程同时往HashMap中put元素,会发生什么?扩容混乱,因为扩容也没有任何锁来保证并发安全

12、HashMap的value允许为null,但是HashTable和ConcurrentHashMap的valued都不允许为null,试分析原因?

13、HashMap中的hook函数(在后面讲解LinkedHashMap时会讲到,这也是面试时拓展的一个点)

参考文献:

https://mp.weixin.qq.com/s/GfB6yerj3fVm_WqSbVCRqA

https://mp.weixin.qq.com/s/I6KFAO1n9IQznieUZL95YA

https://mp.weixin.qq.com/s/EL0lMdYm86B7AG

猜你喜欢

转载自blog.csdn.net/volcano1995/article/details/88667560
今日推荐