equals及hashCode的一点理解

 最近在学习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
uid:123



 通过上图,会有一个疑问,为什么在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的简单理解。







猜你喜欢

转载自blog.csdn.net/cakecc2008/article/details/79231504
今日推荐