文章目录
写作缘由
在使用Set集合时,我们知道,Set存的数据都是不重复的,但是我们又发现,为什么我们存基本类型的数据可以实现去重,但是存引用类型的数据时(String除外),我们设置对象的属性值完全一样,发现并不能实现去重。怀着这个问题,研究了一下Set集合去重的原理,发现hashCode()方法在其中扮演了很重要的作用,于是就对它进行了一下研究。
HashSet的底层实现
- HashSet的底层存储是HashMap
- 往集合中存储元素时,会把元素的值作为key,然后调用hash方法,拿到当前元素对象的hashCode值
- 对存储元素进行判重时,先判断元素的hash值,如果hash值正确,再通过equals()判断添加的元素与哈希值一样的元素是否相同。
以上是HashSet存储元素的大致思路,我们不禁好奇。在我们主观意识上,两个对象的所有属性值相同时,就可以看成是两个相同的对象,通过重写equals()方法判断即可,为什么还要进行一次hash的比较,hash比较之后才进行equals()比较呢?
引出问题
通过查看HashSet的实现,我产生了以下疑问。
- HashCode是什么?HashCode有什么用?
- 为什么对象的比较要先比较HashCode值,再通过equals()比较?
- 同为引用类型,为什么包装类和字符串HashSet会自动去重,而对象就不可以了呢?
1.HashCode是什么
Hash:Hash是散列的意思,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。
Hash表:通过hash算法得到的hash值就在这张hash表中,也就是说,hash表就是所有的hash值组成的。
HashCode:hash算法得到的hash值,每一个hash值在hash表中都有一个对应的位置,称为HashCode。
综上:HashCode就是通过Hash算法生成的Hash值在Hash表中存放的位置。
HashCode和对象的物理地址区别
首先一个对象肯定有物理地址,对象的物理地址跟这个hashcode地址不一样,hashcode代表对象的地址说的是对象在hash表中的位置,物理地址说的对象存放在内存中的地址,那么对象如何得到hashcode呢?
通过对象的内部地址(也就是物理地址)转换成一个整数,然后该整数通过hash函数的算法就得到了hashcode。所以,hashcode是什么呢?就是在hash表中对应的位置。这里如果还不是很清楚的话,举个例子,hash表中有 hashcode为1、hashcode为2、(…)3、4、5、6、7、8这样八个位置,有一个对象A,A的物理地址转换为一个整数17(这是假如),就通过直接取余算法,17%8=1,那么A的hashcode就为1,且A就在hash表中1的位置。
物理地址——转换成整数——hash算法处理——hashcode。
HashCode特性
1、如果散列表中存在和散列原始输入K相等的记录,那么K必定在f(K)的存储位置上
2、不同关键字经过散列算法变换后可能得到同一个散列地址,这种现象称为碰撞
3、如果两个Hash值不同(前提是同一Hash算法),那么这两个Hash值对应的原始输入必定不同
4、如果两个对象不相同,他们的HashCode可能相同,如果两个对象相同,他们的HashCode一定相同。
HashCode作用
HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的(后半句说的用hashcode来代表对象就是在hash表中的位置)
为什么hashcode就查找的更快,比如:我们有一个能存放1000个数这样大的内存中,在其中要存放1000个不一样的数字,用最笨的方法,就是存一个数字,就遍历一遍,看有没有相同得数,当存了900个数字,开始存901个数字的时候,就需要跟900个数字进行对比,这样就很麻烦,很是消耗时间,用hashcode来记录对象的位置,来看一下。hash表中有1、2、3、4、5、6、7、8个位置,存第一个数,hashcode为1,该数就放在hash表中1的位置,存到100个数字,hash表中8个位置会有很多数字了,1中可能有20个数字,存101个数字时,他先查hashcode值对应的位置,假设为1,那么就有20个数字和他的hashcode相同,他只需要跟这20个数字相比较(equals),如果每一个相同,那么就放在1这个位置,这样比较的次数就少了很多,实际上hash表中有很多位置,这里只是举例只有8个,所以比较的次数会让你觉得也挺多的,实际上,如果hash表很大,那么比较的次数就很少很少了。 通过对原始方法和使用hashcode方法进行对比,我们就知道了hashcode的作用,并且为什么要使用hashcode了。
2. 为什么对象的比较要先比较HashCode值,再通过equals()比较?
刚我们看了HashCode的特性,两个对象相同时,他们的HashCode一定相同。但是像个不同的对象的HashCode可能不同。
因此,决定对象相同的两个条件:1.两个对象的HashCode相同。2.两个对象的equals()比较相同。
因此,这也是为什么Java建议我们重写equals方法的时候,HashCode也要跟着重写。
至于为什么先比较HashCode,再比较equals(),这也就很容易解释了。虽然两个不同的HashCode可能重复,但是概率还是很小的,并且在Hash表中获取到某个HashCode的位置的速度,远比在一大群对象了,一个个对比属性值来判定相同要快的多。
所以总结出来就是一句话:通过HashCode迅速找到HashCode相同的对象,再进行equals()比较属性值。提升效率。
3. 同为引用类型,为什么包装类和字符串HashSet会自动去重,而对象就不可以了呢?
源码(以Integer为例)
对于包装类来说,Java本身已经重写了它的HashCode()和equals()方法,因此往集合中添加时,就已经判断了。
Integer类型的数据,它的HashCode值就是他们本身。
public static int hashCode(int value) {
return value;
}
/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is an {@code Integer} object that
* contains the same {@code int} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
HashCode设计原则
通过上面的介绍,我们明白了,判断对象全等的两个要素:1.HashCode相等。2.equals()相等。
在实际的设计中,我们重写hashCode()方法时,我们一定要注意,重写方法内使用的变量一定是不常会发生改变的,最好是不会改变。(id,idcard)等这些,而不要设计成name。
举个栗子:
public class Student {
private String name;
private int age;
@Override
public int hashCode() {
return name.hashCode() + age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
name.equals(student.name);
}
}
测试
Student student1 = new Student();
student1.setName("张三");
student1.setAge(18);
Map<Student,Integer> a = new HashMap<>();
a.put(student1,18);
System.out.println(a.get(student1)); //18
student1.setAge(19);
System.out.println(a.get(student1)); //null
原因很简单,我们修改了student1的属性,因此此时student1的hash值发生了改变,和Map中保存的student1的hash值不同。因此获取不到。
正确方法是为student设置id,equals和hashcode的重写方法内,只添加对id操作,因为id是唯一的,且不可更换的。
public class Student {
private Integer id;
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Student student= (Student) o;
return Objects.equals(id, student.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
hashCode()和System.identityHashCode()的区别
- 一个对象在其生命期中HashCode(identity hash code) 必定保持不变;
- 二者都是返回对象的HashCode编码,在不重写hashCode()方法的前提下,二者返回是一样的。
- 重写hashCode()方法后,对象的hashCode()发生变化,但是真实的地址并没有发生改变。可以通过System.identityHashCode()获取。
总结
- HashCode是对象的物理地址经Hash算法处理后,在Hash表中存放的位置。
- 两个不同的对象可能有相同的HashCode,但是两个对象相同,其HashCode一定相同。
- 重写equal方法时,必须重写HashCode方法。HashCode相等,equals不一定相等。但equals相等,HashCode一定相等。
- 设计HashCode时,无论对象怎么改变,都必须保证该对象的HashCode值保持不变。HashCode不能依赖对象易变的属性。