hashCode()函数详解

上一篇文章介绍了==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:1072743212
V/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:1057947648
V/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:1057947648
V/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(),其他都会迎刃而解。

  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMap 是一种哈希表数据结构,它实现了 Map 接口,可以存储键值对。下面是 JDK 8 中 HashMap 的源码详解。 1. 基本概念 哈希表是一种基于散列原理的数据结构,它通过将关键字映射到表中一个位置来访问记录,以加快查找的速度。在哈希表中,关键字被映射到一个特定的位置,这个位置就称为哈希地址或散列地址。哈希表的基本操作包括插入、删除和查找。 2. 类结构 HashMap 类结构如下: ``` public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... } ``` HashMap 继承了 AbstractMap 类,并实现了 Map 接口,同时还实现了 Cloneable 和 Serializable 接口,表示该类可以被克隆和序列化。 3. 数据结构 JDK 8 中的 HashMap 采用数组 + 链表(或红黑树)的结构来实现哈希表。具体来说,它使用了一个 Entry 数组来存储键值对,每个 Entry 对象包含一个 key 和一个 value,以及一个指向下一个 Entry 对象的指针。当多个 Entry 对象的哈希地址相同时,它们会被放入同一个链表中,这样就可以通过链表来解决哈希冲突的问题。在 JDK 8 中,当链表长度超过阈值(默认为 8)时,链表会被转化为红黑树,以提高查找的效率。 4. 哈希函数 HashMap 的哈希函数是通过对 key 的 hashCode() 方法返回值进行计算得到的。具体来说,它使用了一个称为扰动函数的算法来增加哈希值的随机性,以充分利用数组的空间。在 JDK 8 中,HashMap 使用了以下扰动函数: ``` static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 其中,^ 表示按位异或,>>> 表示无符号右移。这个函数的作用是将 key 的哈希值进行扰动,以减少哈希冲突的概率。 5. 插入操作 HashMap 的插入操作是通过 put() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置是空的,就直接将 Entry 对象插入到该位置;否则,就在该位置对应的链表(或红黑树)中查找是否已经存在具有相同 key 的 Entry 对象,如果存在,则更新其 value 值,否则将新的 Entry 对象插入到链表(或红黑树)的末尾。 6. 查找操作 HashMap 的查找操作是通过 get() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置为空,就直接返回 null;否则,就在该位置对应的链表(或红黑树)中查找是否存在具有相同 key 的 Entry 对象,如果存在,则返回其 value 值,否则返回 null。 7. 删除操作 HashMap 的删除操作是通过 remove() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。然后,在该位置对应的链表(或红黑树)中查找是否存在具有相同 key 的 Entry 对象,如果存在,则将其删除,否则什么也不做。 8. 总结 以上就是 JDK 8 中 HashMap 的源码详解。需要注意的是,哈希表虽然可以加快查找的速度,但是在处理哈希冲突、扩容等问题上也存在一定的复杂性,因此在使用 HashMap 时需要注意其内部实现细节,以便更好地理解其性能和使用方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值