HashCode是什么?
首先,Hash
是指哈希算法算法,这种算法可以输入任意数据,输出固定长度的字符串,这一输出结果就是 HashCode
,即哈希码。这个算法有这样几条规则:
- 同样的输入内容,通过这一算法,总是能够得到相同的结果。
- 不同的输入内容,得到的输出结果可能会相同。
- 如果两次的输出结果不同,则他们的输入内容也一定不同。
在Java/Kotlin中,所有类的基类 Object
/ Any
有一个方法:
1 |
|
这个方法,会返回当前对象的哈希值。这个“当前对象的哈希值”,实际上是用当前对象存储的地址来计算得出的,所以哈希值可以简单的看做一个对象地址的映射。
HashCode有什么用?
基于哈希码的这些特性,很自然的想到,可以用来作为一个对象的唯一标识,来进行对比/查找。在Java中, hashCode()
方法正是这个用途。在Java的集合中,有一种无序但不可重复的集合,比如Set,Map等。这些集合因为插入的对象不能重复,所以在每次插入时都要跟原集合中的对象进行比较,不同或者不存在,才可以插入。如果每次插入时都很粗鲁的遍历一遍集合,每个对象挨个对比,那在集合中元素非常多时,代价会很大, HashCode
便在这里有了用武之地。
具体而言,因为 HashCode
间接的代表了对象的地址,所以每次插入新对象时,可以不用去查找有没有重复的对象,而是直接计算新对象的 HashCode
,然后在指定位置查看是否有对象存在;没有?那就直接插入。有?那就跟这个位置上已经存储的对象进行比较,如果确实是相同的对象,那就不用再进行重复插入了。如果是不同的对象,那还是要插入的,于是会基于这个地址,再使用一个新的地址来存储这个对象。在这一过程汇总,两个对象进行比较时,是用的 equals()
方法,而地址相同后的处理,则被称为 碰撞处理
。
equals
equals()
方法也跟 hashCode()
方法类似,都是基类提供的可供重写的方法,这一方法用来对比两个对象是否相同。这里相同指的是概念上的相同。 equals()
方法默认的实现是:
1 |
|
用 ==
来对比两个对象的地址,这是很简单的对比方式了。在我们实际使用过程中,可能有些场景下,不如如果一个对象他本身地址并没有变,而是内部一些字段发生了变化,那么为了让 equals()
方法能正确的反映这一事实,我们就需要重写 equlas()
方法了。而为了满足前面提到的 hashCode()
的特性,我们在重写 equlas()
方法时,都要同时重写 hashCode()
方法。只有这样,才能保证 hashCode()
方法有效,否则在像集合中这些情况,会有异常。也因此,我们可以得出一个结论:
- 两个对象,
equals()
比较相等,那hashCode()
也一定相等。 - 两个对象的
hashCode()
不同,则equals()
是可能会相同的。
重写 hashCode()
方法
首先要明白,重写 hashCode()
方法,目的在于让计算的结果满足那一特性。除此之外,还要考虑计算出的结果尽量不要发生碰撞,切计算速度快,占用资源少等因素。因此虽然没有规定怎么写,但是我们可以参考 String
类来写:
1 |
|
这一方式在《ffective Java》一书中也有提到,思路都是一样的。下面举个栗子:
1 |
|
这里用的是31,一般用一个不大不小的质数,比如17也可以,这样计算出来的结果长度适中,可以尽量避免碰撞,也不至于太长。这个模板很实用,在日常开发中需要重写时都可以按这个方法来实现。
碰撞
哈希算法有碰撞,而这个碰撞是不能避免的。道理也很简单,输出的数据是定长的,那输入结果必然是有限的,而输入的数据是无限的,所以就像一个经典的生日问题————有n个人不断进入同一个房间,房间内出现生日相同的人的概率问题————一样,随着输入数据的增加,发生碰撞的概率也会越来越高。虽然不能完全避免的,但是可以通过改变输出长度或算法等,尽力来降低碰撞的概率。而一旦真的发生了碰撞,就需要其他方式来解决碰撞了。比较常见的一个栗子是Java中的 HashMap
,他解决碰撞的方法是,Map中Value存储的是一个链表的头节点。当插入的数据不存在碰撞时,他可以直接存在这一Key对应的链表的头结点;当插入的数据已经存在,即发生碰撞时,头节点是已经保存了数据的,那么就跟在头结点后面增加一个节点来保存,以此类推。