最近在学习mapreduce时,自定义了一个AppUidKey类作为自定义key。AppUidKey类需要重写equals方法,重写equals方法就必须重写hashCode方法,所以找了些资料学习,对hashcode有了一点点理解,也不一定全都准确,权当学习笔记
我们知道,两个String对象比较是否相等时,是判断其内容是否相等,而String的父类Object中的equals只有在两个引用变量是否是同一个对象时,即相同的内存地址时,才返回真。所以,需要达到判断内容是否相等,就需要重写equals方法,逐个比较字符串中的字符的ascii码是否相等。以下是String类equals方法的源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
同理,如果我们自己定义一个类,我们需要判断这个类的两个实例是否相等,也需要重写equals方法,如下定义的User类,如果我们不重写equals方法,我们尝试用equals方法比较两个实例,看看是什么效果。
package HashCodeTest;
public class HashCodeTestMain {
public static void main(String[] args) {
//初始化两个我们认为相等的实例
User user1=new User("123");
User user2=new User("123");
System.out.println(user1.equals(user2));
}
}
class User {
String uid=new String();
public User(String uid){
this.uid=uid;
}
}
执行结果:
false
通过测试,我们可以看到,user1.equals(user2)为假,和我们预期的不一样,所以我们需要重写equals方法
package HashCodeTest;
import Test.AppUidKey;
public class HashCodeTestMain {
public static void main(String[] args) {
//初始化两个我们认为相等的实例
User user1=new User("123");
User user2=new User("123");
System.out.println(user1.equals(user2));
}
}
class User {
String uid=new String();
public User(String uid){
this.uid=uid;
}
//重写equals方法
@Override
public boolean equals(Object o) {
if (o instanceof User) {
User that = (User) o;
if(this.uid.equals(that.uid)){
return true;
}else{
return false;
}
}
return false;
}
}
执行结果:
true
如果我们这个User仅仅用于比较是否相等,那这样就可以完事了。但是,很多时候,我们需要将多个User实例,存放到一个迭代容器中使用,比如HashMap。使用以上的代码,我们将User1和User2放到一个HashMap对象中,会是什么情况呢?
package HashCodeTest;
import java.util.HashMap;
import Test.AppUidKey;
public class HashCodeTestMain {
public static void main(String[] args) {
//初始化两个我们认为相等的实例
User user1=new User("123");
User user2=new User("123");
//System.out.println(user1.equals(user2));
HashMap<User,Long> userHashMap=new HashMap<User,Long>();
userHashMap.put(user1, 1L);
userHashMap.put(user2, 1L);
///将两个相等的对象put到HashMap,按照我们的预期,userHashMap只会有一个元素
System.out.println("userHashMap大小:"+userHashMap.size());
for (User user: userHashMap.keySet()) {
System.out.println("uid:"+user.uid);
}
}
}
class User {
String uid=new String();
public User(String uid){
this.uid=uid;
}
//重写equals方法
@Override
public boolean equals(Object o) {
if (o instanceof User) {
User that = (User) o;
if(this.uid.equals(that.uid)){
return true;
}else{
return false;
}
}
return false;
}
}
执行结果:
userHashMap大小:2
uid:123
uid:123
执行结果并没有想我们预期的那样,而是userHashMap中有两个元素,请重明细输出中可以看到,HashMap中的key重复了!这是什么原因呢?要弄清楚这个问题,要先重HashMap的原理开始说起。
这里不会详细全面的阐述HashMap原理,只针对以上userHashMap元素重复问题进行说明。userHashMap在调用put方法时,要先判断当前的key是否已经在userHashMap中。如何判断key是否以及存在,最直接的方法就是调用key对象的equals方法。但是我们知道,对于以上例子,要判断user2的equals方法,是比较uid是否相等,即比较逐个比较uid字符串中的每个字符是否相等。如果是少量的数据可能问题不大,但是如果userHashMap中有上万的元素,每put一个新元素,就要进行上万次的字符串遍历,其性能是很低下的。那么为了提高效率,这就引入了hashcode。
HashMap对象会维护一个hashcode表,用于存储每个月key的hashcode值,这是一个整数值。这样没当新元素加入HashMap时,就用需要新加入的新元素的hashcode到hashcode表中查找一遍,看看是否可以找到。如果未找到就认为这个元素没有在HashMap中,则添加这个元素。如果找到,再调用equals方法。如果equals方法返回假,则认为这个元素没有在HashMap中,否则认为元素已存在。这大大减少了调用equals方法的次数,使HashMap的性能得到提升。
默认的Object的hashCode是对象的内存地址,所以user1、user2两个对象的Object hashCode显然不相等。所以,我们需要在User类中重写我们的hashcode方法。
//重写hashCode方法
@Override
public int hashCode() {
int h=0;
h=this.uid.hashCode();
return h;
}
这时候,我们则测试main方法中的代码时,userHashMap就只有一个元素了,输出结果:
userHashMap大小:1
通过上图,会有一个疑问,为什么在hashcode表中查找已存在,还要再调用equals方法来判断呢?这就谈到了hashcode的原理了。
理论上,我们希望每个对象的hashcode都不相等。但是事实上很难做到,即便做到了,其产生这样hashcode的代价就会变的很高,这样就达不到我们优化性能的初衷了。
总结如下:
1、在一个运行的进程中,相等的对象必须要有相同的哈希码。
2、哈希码相同的对象,不一定相等。
先来看一下这个测试:
System.out.println("Aa".hashCode());
System.out.println("BB".hashCode());
以上两个语句的输出都是2112,即“Aa”和“BB”的hashcode相同。这是为什么呢?我们可以直接查看String类的源代码:
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
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;
}
从代码中我们可以看出String类型的hashcode生成原理,其实与字符串中的字符ASCII码值,及其每一位字符迭代换算而来,以上面的例子来理解:
“Aa”:'A'的ASCII码为65,'a'的ASCII码为97。所以其hashcode值=(0*31+65)*31+97=2112
“BB”:'B'的ASCII码为66,。所以其hashcode值=(0*31+66)*31+66=2112
所以“Aa”与“BB”的hashcode相同。在理解了String类型的hashcode后,我们就会产生两个疑问:
1、为什么hashcode需要这样又乘又加的,直接将两个字符的ASCII相加作为hashcode值不行吗?
2、为什么是乘31而不是其他的数字?
首先来分析第一个问题,如果只是简单的相加,那么我们可以计算下"Aa"与"aA"两个对象的hashcode都将是162。进而只要包含相同字符,数量相等,无论组合顺序如何,它们的hashcode都将相等,这将大大的增加hashcode冲突,这不是我们希望的,我们是希望每个不同对象的hashcode都不一样。所以我们需要进行一些方法来规避这个问题,String类型的hashcode就使用了以上方式。当然我们可以有自己的算法来生成我们自定义对象的hashcode值。
在之前我们定义User类型中,如果除了uid外,还需要另外一个city变量来判断两个实例是否相等,我们需要怎样修改代码呢?equals方法比较容易修改,如下:
//重写equals方法
@Override
public boolean equals(Object o) {
if (o instanceof User) {
User that = (User) o;
if(this.uid.equals(that.uid)&&this.city.equals(that.city)){
return true;
}else{
return false;
}
}
return false;
}
对于hashCode方法,我们能不能这样写呢?
h=this.uid.hashCode()+this.city.hashCode();
答案是不行的,这样的话user1=new User("123","321"); user2=new User("321","123"); 两个对象的hashCode就相等了,原因在上面我们已经分析过。那么该如何修改hashCode方法呢?其实我们可以参考String类的hashCode方法,修改成这样:
//重写hashCode方法
@Override
public int hashCode() {
int h=0;
//h=this.uid.hashCode();
//h=this.uid.hashCode()+this.city.hashCode();
h = h * 31 + this.uid.hashCode();
h = h * 31 + this.city.hashCode();
return h;
}
user1=new User("123","321");
user2=new User("321","123");
System.out.println("user1.hashCode:"+user1.hashCode());
System.out.println("user2.hashCode:"+user2.hashCode());
对user1、user2重新初始化后,输出得到它们的hashCode值不一样。执行结果:
user1.hashCode:1560000
user2.hashCode:1617600
第2个问题,为什么要选择31这个数而不是其他?对于这个问题,因为涉及数学,我还不是很理解,所以只简单谈几点,以后再开主题叙述。
1)因为31是一个质数,在数学理论上,乘以一个质数可以时使哈希码更加离散,减少冲突。
2)理论上,乘以越大的质数可以使哈希码更加离散,但越大的数,计算成本越高,而31是一个比较适中,且冲突率也不高的选项。
3)31这个数字比较接近2的5次方,n*31可以被转换成2<<5-n。将乘法转换成位移+减法,效率可以得到很大的提升。所以jvm中对n*31做了特殊的优化处理。
以上是我作为一个java初学者对于equals和hashCode的简单理解。