Hashcode/Equals
hashcode涉及到集合HashMap等集合,此篇侧重于了解hashcode和equals方法的作用的原理。有助于下一篇HashMap的理解。
一、Hash是什么
Hash
是散列的意思,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。这个玩意还可以做加密。
- 不同关键字经过散列算法变换后可能得到同一个散列地址,这种现象称为碰撞。
- 如果两个
Hash
值不同(前提是同一Hash
算法),那么这两个Hash
值对应的原始输入必定不同
二、什么是hashcode
HashCode
的存在主要是为了查找的快捷性,HashCode
是用来在散列存储结构中确定对象的存储地址的。
如果两个对象的HashCode
相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置。
如果对象的equals
方法被重写,那么对象的HashCode
方法也尽量重写。为什么呢?下文会说。
三、HashCode有什么用
比方说Set
里面已经有1000个元素了,那么第1001个元素进来的时候,最多可能调用1000次equals
方法,如果equals
方法写得复杂,对比的东西特别多,那么效率会大大降低。
使用HashCode
就不一样了,比方说HashSet
,底层是基于HashMap
实现的,先通过HashCode
取一个模,这样一下子就固定到某个位置了,如果这个位置上没有元素,那么就可以肯定HashSet
中必定没有和新添加的元素equals
的元素,就可以直接存放了,都不需要比较;
如果这个位置上有元素了,逐一比较,比较的时候先比较HashCode
,HashCode
都不同接下去都不用比了,肯定不一样,HashCode
相等,再equals
比较,没有相同的元素就存,有相同的元素就不存。
如果原来的Set
里面有相同的元素,只要HashCode
的生成方式定义得好(不重复),不管Set
里面原来有多少元素,只需要执行一次的equals
就可以了。这样一来,实际调用equals
方法的次数大大降低,提高了效率。
四、为什么重写Object的equals()方法尽量要重写Object的hashCode()方法
面临问题:若两个对象equals
相等,但由于不在一个区间,因为hashCode
的值在重写之前是对内存地址计算得出,所以根本没有机会进行比较,会被认为是不同的对象(这就是为什么还要重写hashcode
方法了)。所以Java
对于eqauls
方法和hashCode
方法是这样规定的:
1 如果两个对象相同(
equals
为true
),那么它们的hashCode
值一定要相同。也告诉我们重写equals
方法,一定要重写hashCode
方法,也就是说hashCode
值要和类中的成员变量挂上钩,对象相同–>成员变量相同—->hashCode
值一定相同。
2 如果两个对象的
hashCode
相同(只是映射到同一个位置而已),它们并不一定相同,这里的对象相同指的是用eqauls
方法比较。
简单来说,如果只重写equals
方法而不重写hashcode
方法,会导致重复元素的产生。具体通过下面的例子进行说明。
五、举例
6.1 Student类
很简单,定义了id
和name
两个字段,无参和有参构造函数,toString
方法。
1 | public class Student { |
6.2 main方法
1 | public static void main(String[] args) { |
执行结果:
1 | set集合容量为: 3 |
我们可以看到,只要是new的对象,他们的hashcode
是不一样的。所以,就会认为他们是不一样的对象。所以,集合里面数量为3.
6.3 只重写equals()方法,而不重写HashCode()方法
输出:
1 | set集合容量为: 3 |
结论:覆盖equals
(Object obj
)但不覆盖hashCode()
,导致数据不唯一性。
在这里,其实我们可以看到,student1
和student2
其实是一个对象,但是由于都是new并且没有重写hashcode
导致他们变成了两个不一样的对象。
分析:
(1)当执行
set.add(student1)
时,集合为空,直接存入集合;
(2)当执行
set.add(student2)
时,首先判断该对象(student2
)的hashCode
值所在的存储区域是否有相同的hashCode
,因为没有覆盖hashCode
方法,所以jdk使用默认Object
的hashCode
方法,返回内存地址转换后的整数,因为不同对象的地址值不同,所以这里不存在与student2
相同hashCode
值的对象,因此jdk
默认不同hashCode
值,equals
一定返回false
,所以直接存入集合。
(3)当执行
set.add(student3)
时,与2同理。
(4)当最后执行
set.add(student1)
时,因为student1
已经存入集合,同一对象返回的hashCode
值是一样的,继续判断equals
是否返回true,因为是同一对象所以返回true
。此时jdk认为该对象已经存在于集合中,所以舍弃。
6.4 只重写HashCode()方法,equals()方法直接返回false
1 | set集合容量为: 3 |
按照上面的分析,可能会觉得里面应该装4个,因为两次add的student1,虽然他们的hashcode
一样,但是equals
直接返回false
,那么应该判定为两个不同的对象。但是结果确跟我们预想的不一样。
分析:
首先
student1
和student2
的对象比较hashCode
,因为重写了HashCode
方法,所以hashcode
相等,然后比较他们两的equals
方法,因为equals
方法始终返回false
,所以student1
和student2
也是不相等的,所以student2
也被放进了set
首先
student1(student2)
和student3
的对象比较hashCode
,不相等,所以student3
放进set
中
最后再看最后重复添加的
student1
,与第一个student1
的hashCode
是相等的,在比较equals
方法,因为equals
返回false
,所以student1
和student4
不相等;同样,student2
和student4
也是不相等的;student3
和student4
的hashcode
都不相等,所以肯定不相等的,所以最后一个重复的student1
应该可以放到set
集合中,那么结果应该是size:4
,那为什么会是3呢?
这时候我们就需要查看HashSet
的源码了,下面是HashSet
中的add
方法的源码:
1 | public boolean add(E e) { |
这里我们可以看到其实HashSet
是基于HashMap
实现的,我们在点击HashMap
的put
方法,源码如下:
1 | public V put(K key, V value) { |
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
首先是判断hashCode
是否相等,不相等的话,直接跳过,相等的话,然后再来比较这两个对象是否相等或者这两个对象的equals
方法,因为是进行的或操作,所以只要有一个成立即可,那这里我们就可以解释了,其实上面的那个集合的大小是3,因为最后的一个r1没有放进去,以为r1==r1返回true的,所以没有放进去了。所以集合的大小是3,如果我们将hashCode
方法设置成始终返回false的话,这个集合就是4了。
6.5 同时重写
我的写法是:
1 |
|
结果:
1 | set集合容量为: 2 |
达到我们预期的效果。
六、内存泄露
我们上面实验了重写equals
和hashcode
方法,执行main
,执行结果是:
1 | set集合容量为: 2 |
将main
方法改为:
1 | public static void main(String[] args) { |
运行结果是:
1 | set集合容量为: 2 |
我们调用了remove
删除student3
对象,以为删除了student3
,但事实上并没有删除,这就叫做内存泄露,就是不用的对象但是他还在内存中。所以我们多次这样操作之后,内存就爆了。
原因:
在调用remove
方法的时候,会先使用对象的hashCode
值去找到这个对象,然后进行删除,这种问题就是因为我们在修改了对象student3
的id
属性的值,又因为RectObject
对象的hashCode
方法中有id
值参与运算,所以student3
对象的hashCode
就发生改变了,所以remove
方法中并没有找到student3了,所以删除失败。即student3
的hashCode
变了,但是他存储的位置没有更新,仍然在原来的位置上,所以当我们用他的新的hashCode
去找肯定是找不到了。
总结:
上面的这个内存泄露告诉我一个信息:如果我们将对象的属性值参与了hashCode
的运算中,在进行删除的时候,就不能对其属性值进行修改,否则会出现严重的问题。
七、总结
hashCode
是为了提高在散列结构存储中查找的效率,在线性表中没有作用。equals
和hashCode
需要同时覆盖。- 若两个对象
equals
返回true,则hashCode
有必要也返回相同的int数。 - 若两个对象
equals
返回false,则hashCode
不一定返回不同的int数,但为不相等的对象生成不同hashCode
值可以提高哈希表的性能。 - 若两个对象
hashCode
返回相同int数,则equals
不一定返回true。 - 同一对象在执行期间若已经存储在集合中,则不能修改影响
hashCode
值的相关信息,否则会导致内存泄露问题。
整理自: