1. 什么是 hash 函数

hash 函数,即散列函数,或叫哈希函数。它可以将不定长的输入,通过散列算法转换成一个定长的输出,这个输出就是散列值。需要注意的是,不同的输入通过散列函数,也可能会得到同一个散列值。因此我们不能使用散列函数来获取唯一值。

2. HashMap 为什么要使用 hash 函数

Java 的 HashMap 中使用的是数组 + 链表的结构,但在保存时,一个 K - V 键值对应该被存放到数组的哪个位置?

通常我们都会想到:按照存入顺序存放。但是,按照这种策略,在取值时势必需要遍历整个数组,然后一个个去比较它们的 key 是否相等,这对于性能的损耗无疑是很大的。也许你已经猜到了,解决这个问题的办法就是散列函数。

3. 常见的 hash 算法及冲突的解决

在具体介绍 HashMap 如何使用散列函数之前,先简单介绍一下常见的 hash 算法,以便于你可以更加系统地了解它。

a. 直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址(H(k)=ak+b)。

b. 数字分析法:提取关键字中取值比较均匀的数字作为哈希地址(如一组出生日期,相较于年-月,月-日的差别要大得多,可以降低冲突概率)

c. 分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。

d. 平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。

e. 伪随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。

f. 除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址(H(k)=k%p, p<=m; p一般取m或素数)。

上文已经说到,不同的输入通过散列函数,有可能会得到相同的输出。既然通过不同的输入可以得到相同的输出,那么如果发生冲突了怎么办?比如在 HashMap 中,如果两个不同的 key 计算得出的散列值相同,后来的岂不是会覆盖先来的?不用担心,解决 hash 冲突的方法也是有的,常见的有:

a. 链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

b. 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。

c. 再哈希法:即发生冲突时,由其他的函数再计算一次哈希值。

d. 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。

可能你已经注意到,HashMap 就是使用链地址法来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同)。数组中的每一个单元都会指向一个链表,如果发生冲突,就将 put 进来的 K- V 插入到链表的尾部。

4. HashMap 是如何使用 hash 函数的

首先,我们来看一下在 HashMap 中,最常用的 put() 和 get() 是怎么使用 hash() 的。以下源码均为 jdk7。

  1. // put()
  2. int hash = hash(key.hashCode());
  3. int i = indexFor(hash, table.length);
  4. // get()
  5. int hash = hash(key.hashCode());
  6. for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
  7. Object k;
  8. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
  9. return e.value;
  10. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

可以看到,HashMap 中都是先使用 hash 函数获取一个 hash 值,然后利用得到的 hash 值和容器容量(table.length)计算对象的存放位置(indexFor() 方法)。我们再详细看一下 hash() 和 indexFor() 两个方法。

  1. static int hash(int h) {
  2. return h ^ (h >>> 7) ^ (h >>> 4);
  3. }
  4. static int indexFor(int h, int length) {
  5. return h & (length-1);
  6. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过 put() 方法和 get() 方法,我们可以知道,hash() 方法中的参数 h, h = key.hashCode,hash() 方法对 hashCode 分别无符号右移 (>>>) 7 位和 4 位,再与自身进行异或(^)处理。 这么做的目的是什么?

由于 indexFor() 返回的是 h(hash 值) 与 length - 1(容器容量 - 1) 进行按位与运算的结果,若不进行扰动,即 h = key.hashCode(注意,这里的 h 是 indexFor() 方法的参数,即 hash() 方法的返回值,而非 hash() 方法的参数 h),这将会很容易发生冲突。如下图所示,当低位相同时, h & (length - 1) 结果也会是一样的。即 indexFor() 的计算结果只与 hashCode 的低位相关。

在经过扰动算法后,结果如下:

可以明显看到,计算出来的 hash 值不一样了,即二者不会再发生冲突。这就是为什么 hash() 方法中要使用扰动算法:可以有效降低冲突概率。

既然已经解决了 hash() 的计算问题,那么接下来就是计算索引了。

HashMap 通过 hash 值与 length-1 (容器长度-1)进行取模(%)运算。可能有人会问:明明源码中 indexFor() 方法进行的 按位与(&)运算,而非取模运算。

实际上,HashMap 中的 indexFor() 方法就是在进行取模运算。利用位运算代替取模运算,可以大大提高程序的计算效率。位运算可以直接对内存数据进行操作,不需要转换成十进制,因此效率要高得多。

需要注意的是,只有在特定情况下,位运算才可以转换成取模运算(当 b = 2^n 时,a % b = a & (b - 1) )。也是因此,HashMap 才将初始长度设置为 16,且扩容只能是以 2 的倍数(2^n)扩容。

5. 总结

a. hash 函数并不能保证得到唯一的输出值,不同的输入也有可能得到相同的输出。

b. HashMap 中的 hash() 方法,将 hashCode 的高位和低位混合起来,降低冲突概率。

c. HashMap 中解决冲突的办法是采用链地址法(jdk7)。

d. HashMap 的初始长度为 16,且每次扩容都必须以 2 的倍数(2^n)扩充。因为在 HashMap 中,采用按位与运算(&)代替取模运算(&),当 b = 2^n 时,a % b = a & (b - 1) 。

HashMap 中的 hash 函数的更多相关文章

  1. HashMap中的hash函数

    在写一个HashSet时候有个需求,是判断HashSet中是否已经存在对象,存在则取出,不存在则add添加.HashSet也是通过HashMap实现,只用了HashMap的key,value都存储一个 ...

  2. hashCode及HashMap中的hash()函数

    一.hashcode是什么 要理解hashcode首先要理解hash表这个概念 1. 哈希表 hash表也称散列表(Hash table),是根据关键码值(Key value)而直接进行访问的数据结构 ...

  3. [ 转载 ]hashCode及HashMap中的hash()函数

    hashCode及HashMap中的hash()函数   一.hashcode是什么 要理解hashcode首先要理解hash表这个概念 1. 哈希表 hash表也称散列表(Hash table),是 ...

  4. HashMap中的hash算法总结

    前言 算法一直是我的弱项,然而面试中基本是必考的项目,刚好上次看到一个HashMap的面试题,今天也来学习下 HashMap中的hash算法是如何实现的. 数学知识回顾 << : 左移运算 ...

  5. 深入理解HashMap(及hash函数的真正巧妙之处)

    原文地址:http://www.iteye.com/topic/539465 Hashmap是一种非常常用的.应用广泛的数据类型,最近研究到相关的内容,就正好复习一下.网上关于hashmap的文章很多 ...

  6. HashMap中的hash算法中的几个疑问

    HashMap中哈希算法的关键代码 //重新计算哈希值 static final int hash(Object key) { int h; return (key == null) ? 0 : (h ...

  7. 【Java深入研究】11、深入研究hashmap中的hash算法

    一.简介 大家都知道,HashMap中定位到桶的位置 是根据Key的hash值与数组的长度取模来计算的. JDK8中的hash 算法: static final int hash(Object key ...

  8. K:HashMap中hash函数的作用

      在分析了hashCode方法和equals方法之后,我们对hashCode方法和equals方法的相关作用有了大致的了解.在通过查看HashMap类的相关源码的时候,发现其中存在一个int has ...

  9. 【转】【java源码分析】Map中的hash算法分析

    全网把Map中的hash()分析的最透彻的文章,别无二家. 2018年05月09日 09:08:08 阅读数:957 你知道HashMap中hash方法的具体实现吗?你知道HashTable.Conc ...

随机推荐

  1. Python:爬取中国各市的疫情数据并存储到数据库

    import requests import pymysql import json def create(): # 连接数据库 db = pymysql.connect(host = 'localh ...

  2. 定时-TimerTask

    /** * @param args * @throws InterruptedException */ public static void main(String[] args) throws In ...

  3. this和super的区别和应用

    A:this和super都代表什么 * this:代表当前对象的引用,谁来调用我,我就代表谁 * super:代表当前对象父类的引用B:this和super的使用区别 * a:调用成员变量  * th ...

  4. vue单文件组件形成父子(子父)组件之间通信(vue父组件传递数据给子组件,子组件传递数据给父组件)

    看了很多文章,官网文档也有看,对父子组件通信说的不是很明白:决定自己总结一下: vue一般都使用构建工具构建项目:这样每个组件都是单文件组件:而网上很多文章都是script标签方式映入vue,组件通信 ...

  5. 文档声明(Doctype)和<!Doctype html>有何作用? 严格模式与混杂模式如何区分?它们有何意义?

    文档声明的作用: 文档声明是为了告诉浏览器,当前HTML文档使用什么版本的HTML来写的,这样浏览器才能按照声明的版本来正确的解析. <!doctype html> 的作用就是让浏览器进入 ...

  6. Azure DevOps (九) 通过流水线推送镜像到Registry

    上一篇文章我们研究了如何通过流水线编译出一个docker的镜像,本篇我们来研究一下,如何把编译好的镜像推送到镜像仓库去. 平时如果我们是单机部署,我们的docker本身就装在部署的机器上,我们在本机直 ...

  7. typora简单使用手册

    typora简单使用手册讲解`` 下载网站 网址:https://typoraio.cn/ 苹果电脑:https://typora.en.softonic.com/ 正版呢当然是收费 破解版自行百度 ...

  8. Ant Design Pro V5 与 IdentityServer 实现 Password 模式的登录

    最近处于休息状态,想趁着休息时间,为自己做一个后台. 后端框架选用了 Abp.之前公司使用了一些自研的框架,但由于人力资源有限,后期框架的升级及维护都是比较耗时,这次干脆直接使用Abp,即省心又能快速 ...

  9. HCIE-SEC笔记-第四节-网络入侵和防火墙基础

    等级保护: 网络安全:防火墙.VPN.准入控制 渗透测试: 防火墙:区域隔离和访问控制 数字与研究公司:用数据说话 IDC:国际数据公司 Gartner:著名的数字与咨询公司 弗雷斯特: 数世咨询: ...

  10. js 修改页面样式的两种方式

    1.  element.style       行内样式操作 代码示例 : <!DOCTYPE html> <html lang="en"> <hea ...