Java基础之HashCode

概述

介绍Java中的HashCode相关概念及其生成方法。

1. HashCode概念

1.1 HashCode定义

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。换句话说,哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同。也有相同的情况,看程序员如何写哈希码的算法。

后文将会详细介绍 public int hashCode()返回该对象的哈希码值。
支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。

1.2 HashCode约定

约定
说明
一致性在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行hashcode比较时所用的信息没有被修改。
equals如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
注:这里说的equals(Object) 方法是指Object类中未被子类重写过的equals方法。
由于是根据对象的特征生成的,因此存在一定几率,即使两个hashCode()返回的结果相等,两个对象的equals方法也不一定相等。
附加如果根据equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法可能生成相同的整数结果。
但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

1.3 常见的HashCode的生成算法

类别
生成算法
Object类的hashCode返回对象的内存地址经过处理后的结构,由于每个对象的内存地址都不一样,所以哈希码也不一样。
String类的hashCode根据String类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串所在的堆空间相同,返回的哈希码也相同。
Integer类的hashCode返回的哈希码就是Integer对象里所包含的那个整数的数值,例如Integer i1=new Integer(100),i1.hashCode的值就是100 。由此可见,2个一样大小的Integer对象,返回的哈希码也一样。

1.4 重写equals()和hashCode()

对于所有的类,其基类都是Object,因此在未重写equals()之前,都是使用的Object的方法,比较的是对象的内存地址生成的哈希值。

1.4.0 重写原因

这里引用参考重写equals就必须重写hashCode的原理分析

如果不被重写(原生Object)的hashCode和equals是什么样的参见1.4.1

  • 不被重写(原生)的hashCode值是根据内存地址换算出来的一个值。
  • 不被重写(原生)的equals方法是严格判断一个对象是否相等的方法(object1 == object2)。

为什么需要重写equals()?

  • 在我们的业务系统中判断对象时有时候需要的不是一种严格意义上的相等,而是一种业务上的对象相等。在这种情况下,原生的equals方法就不能满足我们的需求了
  • 所以这个时候我们需要重写equals方法,来满足我们的业务系统上的需求。

为什么在重写equals方法的时候需要重写hashCode方法呢?

我们先来看一下Object.hashCode的通用约定(摘自《Effective Java》第45页)

  1. 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,那么,对该对象调用hashCode方法多次,它必须始终如一地返回 同一个整数。在同一个应用程序的多次执行过程中,这个整数可以不同,即这个应用程序这次执行返回的整数与下一次执行返回的整数可以不一致。
  2. 如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一个对象的hashCode方法必须产生同样的整数结果。
  3. 如果两个对象根据equals(Object)方法是不相等的,那么调用这两个对象中任一个对象的hashCode方法,不要求必须产生不同的整数结果。然而,程序员应该意识到这样的事实,对于不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。
  1. 如果只重写了equals方法而没有重写hashCode方法的话,则会违反约定的第二条:相等的对象必须具有相等的散列码(hashCode)。

同时对于HashSet和HashMap这些基于散列值(hash)实现的类。HashMap的底层处理机制是以数组的方法保存放入的数据的(Node<K,V>[] table),其中的关键是数组下标的处理。数组的下标是根据传入的元素hashCode方法的返回值再和特定的值异或决定的。如果该数组位置上已经有放入的值了,且传入的键值相等则不处理,若不相等则覆盖原来的值,如果数组位置没有条目,则插入,并加入到相应的链表中。检查键是否存在也是根据hashCode值来确定的。

  1. 如果不重写hashCode的话,可能导致HashSet、HashMap不能正常的运作

如果我们将某个自定义对象存到HashMap或者HashSet及其类似实现类中的时候,如果该对象的属性参与了hashCode的计算,那么就不能修改该对象参数hashCode计算的属性了。有可能会移除不了元素,导致内存泄漏。

我们先看看这两个方法在Object里面是如何实现的:

1.4.1 Object中的equals()和hashCode()

equals()将某对象拿来和原始对象进行对比,如果它俩指向同一个对象,那么返回true,否则返回false;

    public boolean equals(Object obj) {
            return (this == obj);
        }

hashCode()可以看到,这个方法只有一个定义,而没有具体的实现。然而,它有一个native关键字,说明其实现是在C++中,而不是在本地。在C++中,hashCode值是根据内存地址换算出来的一个值。

public native int hashCode();

"native"关键字是JNI的一部分
JNI是Java Native Interface的 缩写。从Java 1.1开始,Java Native Interface (JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计 的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。
在这里插入图片描述
声明native方法如果你想将一个方法做为一个本地方法的话,那么你就必须声明改方法为native的,并且不能实现。
参考全面了解Java中Native关键字的作用

1.4.2 重写equals()和hashCode()

重写equals()目的是让对象按不同的方式进行比较。

  • 对于String类,它重写了equals()方法,将原本的hash值比较变成了字符串值比较(字符串->字符数组->先比较数组长度相等->逐个字符比较)。见附录
  • 对于Integer类,它也重写了equals()方法,将原本的Hash值比较变成了数值的比较。见附录
  • 对于自定义类,首先要确定Object的equals()方法是否适合,如果不适合的话,那么我们就需要重写equals()方法。
  • 其他

重写hashCode()

  • 只要重写 equals,就必须重写 hashCode
    • 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。
  • ** 如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals。**。

接下来,以String类为例,我们说明下String类对于Object()中方法的重写。

1.4.2 String类重写equals()、hashCode()、toString()

重写equals()方法:

public boolean equals(Object anObject) {
        if (this == anObject) {//如果两个值的引用相同,直接返回true。
            return true;
        }
        if (anObject instanceof String) {// 确认比较对象也是String类型 
            String anotherString = (String) anObject;
            int n = value.length;
            if (n == anotherString.value.length) {// 先比较两个数组的长度是否相等
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {// 如果相同,把这两个字符数组里的内容逐个进行比较
                    if (v1[i] != v2[i])
                            return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

重写hashCode()方法把String 字符串先转换成 字符数组,遍历每一个字符,使用“直接寻址法”计算,最终返回一个 int 类型的哈希值。

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
 
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

在Object中,还有一个经常被重写的方法是toString()

直接输出一个类的对象的时候,会调用这个类的toString()方法,这个方法有些类是覆盖了的,比如String,Integer。

你自己写的类没有覆盖这个方法的话就是继承Object类的这个方法,Object中toString()方法的实输出格式是这样的getClass().getName() + “@” + Integer.toHexString(hashCode()) 后面跟的是这个类的哈希码,如果你希望这个类打印出来输出你希望的格式,你就要覆盖这个、toString方法。

2. Hash函数 (散列函数)

2.1 定义

Hash,一般翻译做散列、杂凑,或音译为哈希,是任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

在哈希表中,定义如下
  若结构中存在和关键字K相等的记录,则必定在f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function),这个事先建立的表为散列表

2.2 特点

Hash碰撞(冲突)
  对不同的关键字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),这种现象称碰撞。

  • 如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的:所有散列函数都有此特性。这个特性是散列函数具有确定性的结果。但另一方面,散列函数的输入和输出不是一一对应的,如果两个散列值相同,两个输入值很可能是相同的,但不绝对肯定二者一定相等(可能出现哈希碰撞)。输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。
  • 典型的散列函数都有无限定义域:比如任意长度的字节字符串,和有限的值域,比如固定长度的比特串。在某些情况下,散列函数可以设计成具有相同大小的定义域和值域间的一一对应。一一对应的散列函数也称为排列。可逆性可以通过使用一系列的对于输入值的可逆“混合”运算而得到。

2.3 优点

  • 提高存储空间的利用率:Hash转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。
  • 难以逆向:Hash算法可以将一个数据转换为一个标志,这个标志和源数据的每一个字节都有十分紧密的关系。Hash算法还具有一个特点,就是很难找到逆向规律。
  • 可以提高查询效率
  • 可以做数字签名来保障数据传递的安全性
  • Hash算法是一个广义的算法,也可以认为是一种思想,也被称为散列算法。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。

2. Hash函数设计考虑因素

  1. 计算散列地址所需要的时间(即hash函数本身不要太复杂)
  2. 关键字的长度
  3. 哈希表的大小
  4. 关键字分布是否均匀,是否有规律可循
  5. 记录的查找频率
  6. 设计的hash函数在满足以上条件的情况下尽量减少冲突

2. 常用的Hash函数

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。常用Hash函数有:

函数
说明
直接寻址法取关键字或关键字的某个线性函数值为散列地址。
即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)
数字分析法数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
例如分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,
这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,
如果用后面的数字来构成散列地址,则冲突的几率会明显降低。
平方取中法取关键字平方后的中间几位作为散列地址。
折叠法将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
随机数法选择一随机函数,取关键字作为随机函数的种子生成随机值作为散列地址。
通常用于关键字长度不同的场合。
除留余数法取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。
即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。
对p的选择很重要,一般取素数或m,若p选的不好,容易产生碰撞。

关于具体的使用、适用说明,可以参考数据结构 Hash表(哈希表)

3. 哈希碰撞(冲突)及解决方法

具有相同函数值的关键字对该哈希函数来说称为同义词(synonym)

对不同的关键字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),这种现象称碰撞。

不管hash函数设计的如何巧妙,总会有特殊的key导致hash冲突,特别是对动态查找表来说。hash函数解决冲突的方法有以下几个常用的方法:

方法名
说明
开放定址法当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。
沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)
查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。
链地址法将所有关键字为同义词的结点链接在同一个单链表中。
若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0…m-1]。
凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。
T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
公共溢出区法建立一个特殊存储空间,专门存放冲突的数据。此种方法适用于数据和冲突较少的情况。
再散列法准备若干个hash函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数,第二个也冲突,使用第三个……

主要使用的是开放定址法和链地址法,下面详细介绍下这两种方法。

3.1 开放定址法

TODO - 数据结构 Hash表(哈希表)
TODO - 解决哈希(HASH)冲突的主要方法

3.2 链地址法

TODO - 数据结构 Hash表(哈希表)
TODO - 解决哈希(HASH)冲突的主要方法

其他

参见百度百科-Hash (散列函数)

致谢

附录

---------Integer.equals()------------------
public boolean equals(Object obj) {
  if (obj instanceof Integer) {
    return value == ((Integer)obj).intValue();
  }
  return false;
}
---------------------------------------------

-------------String.equals()-----------------------------------------
 public boolean equals(Object anObject) {
        if (this == anObject) {//如果两个值的引用相同,直接返回true
            return true;
        }
        if (anObject instanceof String) {//如果是String类型 
            String anotherString = (String) anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {//把这两个数组里的内容逐个进行比较
                    if (v1[i] != v2[i])
                            return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
----------------------------------------------------------------------    
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值