介绍
hashCode()
和equals()
常在List、Map中常常用到这两个方法,List用的是equals()
来决定List中存储的是否为同一个对象,Map用的是equals()
和hashCode()
来决定Map中存储的是否为同一个对象。由于Map是key-value组成的,涉及到key和value的映射关系,那么就会用到这两个方法去确定它们的映射关系。在创建类过程中,我们一般不用去复写这两个方法,但是有些业务需求要求我们不得不去重写这两个方法来解决
equals()
创建Student实体类
public class Student {
int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
对两个对象设置相同的id
Student s1 = new Student();
Student s2 = new Student();
s1.setId(10);
s2.setId(10);
Log.e("TAG", "" + s1.equals(s2));// false
List<Student> list = new ArrayList<>();
list.add(s1);
Log.e("TAG", "" + list.contains(s2));// false
在输出的结果中,两个对象比较的返回值为false,集合List也不是两个相同对象,可以看出这两个Student对象不是同一个对象。但很多种情况下,我们的业务需求要求我们同样的id只能存在一个对象,所以这个时候我们就必须重写我们的equals()
方法,也可以通过AS自动生成equals()
。重写equals()
后,这个时候s1.equals(s2)
输出结果为ture,list.contains(s2)
输出结果也为ture。重写的equals()
代码如下
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id;
}
hashCode()
还是用上面的例子,不过这一次会对上次的例子做下修改
Set<Student> set = new HashSet<>();
set.add(s1);
set.add(s2);
Log.e("TAG", "size:" + set.size());// size:2
通过输出的结果得出,s1和s2相同的对象,按照我们的预期,size()
应该是1才对,问题在哪里呢?由于Set和Map的集合类实现原理都是先根据key的hashCode()
来计算存储位置的,确定完存储位置后才调用equal()
来存储,所以在存储过程中size()
返回2。这时候,我们就需要重写hashCode()
来解决这个问题,用AS自动生成hashCode()
。重写hashCode()
后,Set的存储就保持一致了,size()
的输出就为1了,到这里我们基本上解决了业务需求的问题。重写的equals()
和hashCode()
代码如下
public class Student {
int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id;
}
@Override
public int hashCode() {
return id;
}
}
重写规则
AS自动生成的代码,重写的规则是怎么样的呢?我们通过多种类型的变量来看看具体的重写规则
public class Person {
boolean personBoolean;
byte personByte;
char personChar;
short personShort;
long personLong;
float personFloat;
double personDouble;
String personString = "AAA";
String[] personStrings = new String[]{};
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (personBoolean != person.personBoolean) return false;
if (personByte != person.personByte) return false;
if (personChar != person.personChar) return false;
if (personShort != person.personShort) return false;
if (personLong != person.personLong) return false;
if (Float.compare(person.personFloat, personFloat) != 0) return false;
if (Double.compare(person.personDouble, personDouble) != 0) return false;
if (!personString.equals(person.personString)) return false;
// Probably incorrect - comparing Object[] arrays with Arrays.equals
return Arrays.equals(personStrings, person.personStrings);
}
@Override
public int hashCode() {
int result;
long temp;
result = (personBoolean ? 1 : 0);
result = 31 * result + (int) personByte;
result = 31 * result + (int) personChar;
result = 31 * result + (int) personShort;
result = 31 * result + (int) (personLong ^ (personLong >>> 32));
result = 31 * result + (personFloat != +0.0f ? Float.floatToIntBits(personFloat) : 0);
temp = Double.doubleToLongBits(personDouble);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + personString.hashCode();
result = 31 * result + Arrays.hashCode(personStrings);
return result;
}
}
equals()主要是通过对象值的判断,而hashCode()根据不同的数据类型生成的规则是不一样的。hashCode()的重写不能太过简单,否则哈希冲突过多。也不能太过复杂,否则计算复杂度过高,影响性能。其生成规则如下
- 如果是boolean类型,计算(f?1:0)
- 如果是byte、char、short或者int类型,计算(int)f
- 如果是long类型,计算(int)(f^(f>>>32))
- 如果是float类型,计算Float.floatToIntBits(f)
- 如果是double类型,计算Double.doubleToLongBits(f),然后重复第三个步骤。
- 如果是对象引用,并且该类的equals方法通过递归调用equals方法来比较这个域,同样为这个域递归的调用hashCode,如果这个域为null,则返回0。
- 如果是数组,要把每一个元素当作单独的域来处理,递归的运用上述规则,如果数组域中的每个元素都很重要,那么可以使用Arrays.hashCode方法。
String源码中hashCode()
String类的hashcode()算法充分利用了字符串内部字符数组的所有字符
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
生成hash码的算法在String类中如下所示,注意“s“是那个字符数组,n是字符串的长度
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
hashCode()为什么使用31数字作为乘子
31在数学中属于素数。素数的作用就是在使用的时候,如果我用一个数字来乘以这个素数,那么最终的出来的结果只能被素数本身和被乘数还有1来整除。这样子就会有人有疑问了,其他7,13,33这些素数为什么不可以使用呢?其实是有原因的,31是通过验证后,取的是一个折中的结果。具体有什么原因,我们可以取这两个观点进行分析:
- 选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) – i,现代的 Java 虚拟机可以自动的完成这个优化。
- 由于hashCode()方法返回int值,其最大值就是65535,那么就可以打个比方:如果你对超过50,000个英文单词进行hashcode运算,并使用常数31,33,37,39和41作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面几个常数中,常数31被Java实现所选用也就不足为奇了。
总结
- 存储在List的对象,是以equals()去判断是否为同一个对象
- 存储在Map的对象,是以key的hashCode()和value的equals()去判断是否为同一个对象