hashCode()函数详解

来源:互联网 发布:打印文件软件 编辑:程序博客网 时间:2024/06/06 07:50

上一篇文章介绍了==equals()的区别,在其中提到了重写equals()的同时需要重写hashCode()函数,本篇文章主要是对hashCode()做一个详细的介绍,包括其存在的意义以及如何去重写hashCode().

hashCode()存在的意义


在Java的Object类中有一个方法hashCode()

public native int hashCode();

但是hashCode()函数存在的意义是什么呢?先举一个例子,假如一个列表中存储了十万个对象,现在我们需要往其中插入一个对象A,若列表中已经存在了一个与A相等的对象,则不进行插入,否则就插入到列表中,也许我们很快就会想到equals(),调用equals()(这里假设已经重写了equals())来比较对象是否相等,然后执行插不插入的操作。初一看没错,能实现需求呀!但是回过头一想,十万条数据逐个去调用equals()是不是相等,性能就不用说了哈!

为了解决此类问题,散列集合就诞生了,原理则是通过对象生成一个key,然后再通过内部映射到集合的某个位置,当这个位置上已经存在对象的时,调用equals()来进行比较是否真正相等,若相等则说明存在,不相等说明不存在,然后按照集合的具体存储结构来进行存储。

散列集合中,判断集合中是否存在相等的对象时,需要经过3个步骤:
1.通过对象生成一个key
2.将key在内部映射到一个具体的位置
3.与步骤2映射的位置上的对象进行比较(调用equals()

在Java中散列集合包括HashSetHashMap以及HashTable,而其中的key就是通过对象的hashCode()函数来生成。

由上面的3个步骤我们可以知道,当不相等的对象生成的key不同时,每个内部映射的位置都不一样,则不需要多次调用equals()进行比较,而不相等的对象可能生成相等的key(hash冲突),这种概率越大,则需要调用equals()的进行比较的次数越多,效率就越低,因此我们要尽可能做到不相等的对象的hashCode()生成的key不相等。


如何重写hashCode()


通过上面的分析,重写hashCode()函数需要注意两点:
1.不相等的对象的hashCode()生成的key要尽可能不一样。
2.相等的对象的hashCode生成的key必须一样(这就是重写equals()必须重写hashCode()的原因所在)

因此hashCode()的重写完全取决了equals()的实现,而equals()的实现最终会回到八种基本数据类型的比较,只要清楚了基本类型如何去实现hashCode(),再复杂的hashCode()重写的原理都是一样的,都能迎刃而解。

而hashCode()的返回类型是int型,因此其它的基本类型都需要在hashCode()转换为int.下面介绍其他基本类型如何来重写hashCode.

byte,short,int,char

byte、short、char三种类型转换为int时,可以直接进行转换且没有精度的损失,实现原理是一样的,看下面的例子

public class User {    short id;    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        User user = (User) o;        return id == user.id;    }}

上面是一个user对象,重写了equals(),其中通过比较short类型的id来判断两个user对象是否相等,根据重写hashCode()的原则,两个相等对象的hashCode()生成的key必须相等,不相等对象的hashCode()生成的对象尽可能不相等,因为是通过id来判断是否相等,因此hashCode()只需要直接将short类型的id转换为int类型返回即可满足。具体重写的hashCode()实现如下

public class User {    short id;    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        User user = (User) o;        return id == user.id;    }    @Override    public int hashCode() {        return (int) id;    }}

byte跟short实现方式完全一样,而int类型不需要转换直接返回即可,具体代码就不贴了。下面看一个稍微复杂一点的。

long

long类型转换为int类型会存在精度的损失,将高位直接丢掉,那么重写hashCode()是否也是直接转换为int类型呢?答案是可以的,满足重写hashCode()的两个原则,将上面的id改为long类型,代码如下

public class User {    long id;    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        User user = (User) o;        return id == user.id;    }    @Override    public int hashCode() {        return (int) id;    }}

代码跟重写short、byte重写hashCode()代码是完全一样的,但是因为long转int会将高32位直接丢掉,导致高位不同、低位相同的id的hashCode()是一样的

public static void testLongHashCode() {    User user1 = new User();    user1.id = 0b0000_0000_1111_1111_1100_0011_0010_1100_0011_1111_1111_0000_1100_0011_0010_1100L;    User user2 = new User();    user2.id = 0b0000_0000_1000_1000_1100_0011_0010_0101_0011_1111_1111_0000_1100_0011_0010_1100L;    Log.v("hashcode","user1 hashCode:" + user1.hashCode());    Log.v("hashcode","user2 hashCode:" + user2.hashCode());}

上面的代码中,user1和user2的id的高32位不一样、低32位一样,分别打印他们的hashCode()生成的值,结果如下

V/hashcode: user1 hashCode:1072743212V/hashcode: user2 hashCode:1072743212

user1和user2的hashCode()生成的值是一样的,而重写hashCode()的原则中,不相等的对象hashCode()生成的key要尽可能不一样,为了避免高32位相同、低32位不同hashCode()生成的key一样,推荐的做法是将高32位和低32位做异或运算,修改User的hashCode的代码

public class User {    public long id;    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        User user = (User) o;        return id == user.id;    }    @Override    public int hashCode() {        return (int)(id ^ (id >>> 32));    }}

稍微解释下,id >>> 32 无符号右移32位,高32位变成低32位,再与原有的id进行异或运算,再强转int取低32位,
还是上面的user1和user2,看看生成的hashCode()是否还一样,运行的结果如下

V/hashcode: user1 hashCode:1057947648V/hashcode: user2 hashCode:1064828937

hashCode()生成的key不一样了,那么这种方法是否一种完美的方案呢?那user1的id不变,修改user2的id试试,其中两个id不相等,代码如下

  public static void testLongHashCode() {    User user1 = new User();    user1.id = 0b0000_0000_1111_1111_1100_0011_0010_1100_0011_1111_1111_0000_1100_0011_0010_1100L;    User user2 = new User();    user2.id = 0b0000_0000_1111_1111_1100_0011_0010_0000_0011_1111_1111_0000_1100_0011_0010_0000L;    Log.v("hashcode","user1 hashCode:" + user1.hashCode());    Log.v("hashcode","user2 hashCode:" + user2.hashCode());}

运行结果如下

V/hashcode: user1 hashCode:1057947648V/hashcode: user2 hashCode:1057947648

哈哈,结果又一样了,其实我们是进行高32位与低32位运算的结果,因此只要对应位上同时发生变化后的结果一样,那么最后hashCode()生成的值还是一样的。

那么针对于long的重写hashCode(),是否能避免生成一样的结果呢?答案是不可能的,因为hashCode()的返回类型为int,long到int的转换存在精度损失,产生冲突是不可避免的(hash冲突)。

boolean

布尔类型的hashCode()重写就简单了,因为不是true就是false,直接转换为1或者0就好了,代码如下

public class User {    public boolean id;    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        User user = (User) o;        return id == user.id;    }    @Override    public int hashCode() {        return (id ? 1 : 0);    }}

Float 与 Double

Float重写hashCode() 直接将float转换为int即可,而double则先转换为long,再通过long 高地位异或的方式实现
Float

public class User {    public float id;    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        User user = (User) o;        return id == user.id;    }    @Override    public int hashCode() {        return (id != +0.0f ? Float.floatToIntBits(id) : 0);    }}

Double

public class User {    public double id;    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        User user = (User) o;        return id == user.id;    }    @Override    public int hashCode() {        long temp = Double.doubleToLongBits(id);        return (int) (temp ^ (temp >>> 32));    }}

当然equals()的实现可能比较复杂,常见的可能是数组的比较、递归之类的,对于数组的比较则需要逐个进行计算,而递归的话则需要进行递归的计算

数组

String就是一个典型的char数组,那么我们分析分析String的hashCode()是如何实现的呢?

    @Override     public int hashCode() {        int hash = hashCode;        if (hash == 0) {            if (count == 0) {                return 0;            }            for (int i = 0; i < count; ++i) {                hash = 31 * hash + charAt(i);            }            hashCode = hash;        }        return hash;    }

通过遍历每一个字符然后计算得出结果,对代码稍微解释一下

为什么要使用“*”?
主要是为了使散列值依赖于域的顺序,如果不适用的话,那么“as”与“sa”的hashCode()生成的值就是一样的了。

为什么是乘以31而不是其他数字?
乘以31不是为了别的,而是31这个数字的特殊性,因为任何数n * 31就可以被JVM优化为 (n << 5) -n,移位和减法的操作效率要比乘法的操作效率高的多。

其他

其他的复杂的equals()实现,最终会回到基本类型的比较来,只要搞懂了基本类型如何去重写hashCode(),其他都会迎刃而解。

原创粉丝点击