java hashcode
equals方法和hashCode方法
Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这两个方法。equals()和hashCode()方法是用来在同一类中做比较用的,尤其是在容器里如set存放同一类对象时用来判断放入的对象是否重复。
常见错误:
import java.util.HashMap; public class Apple { private String color; public Apple(String color) { this.color = color; } public boolean equals(Object obj) { if(obj==null) return false; if (!(obj instanceof Apple)) return false; if (obj == this) return true; return this.color.equals(((Apple) obj).color); } public static void main(String[] args) { Apple a1 = new Apple("green"); Apple a2 = new Apple("red"); //hashMap stores apple type and its quantity HashMap<Apple, Integer> m = new HashMap<Apple, Integer>(); m.put(a1, 10); m.put(a2, 20); System.out.println(m.get(new Apple("green"))); } }
上面的代码执行过程中,先是创建个两个Apple,一个green apple和一个red apple,然后将这来两个apple存储在map中,存储之后再试图通过map的get方法获取到其中green apple的实例。可以试着执行以上代码,数据结果为null。也就是说刚刚通过put方法放到map中的green apple并没有通过get方法获取到。可能怀疑是不是green apple并没有被成功的保存到map中,但是,通过debug工具可以看到,它已经被保存成功了。
造成以上问题的原因其实比较简单,是因为代码中并没有重写hashcode
方法。hashcode
和equals
的约定关系如下:
1、如果两个对象相等,那么他们一定有相同的哈希值(hash code)。
2、如果两个对象的哈希值相等,那么这两个对象有可能相等也有可能不相等。(需要再通过equals来判断)
Map它是通过把key值进行hash来定位对象的,这样可以提供比线性存储更好的性能。实际上,Map的底层数据结构就是一个数组的数组(准确的说其实是一个链表+数组)。第一个数组的索引值是key的哈希码。通过这个索引可以定位到第二个数组,第二个数组通过使用equals方法进行线性搜索的方式来查找对象。
其实,一个哈希码可以映射到一个桶(bucket)中,hashcode
的作用就是先确定对象是属于哪个桶的。如果多个对象有相同的哈希值,那么他们可以放在同一个桶中。如果有不同的哈希值,则需要放在不同的桶中。至于同一个桶中的各个对象之前如何区分就需要使用equals
方法了。
hashcode方法的默认实现会为每个对象返回一个不同的int类型的值。所以,上面的代码中,第二个apple被创建出来时他将具有不同的哈希值。可以通过重写hashCode方法来解决。
public int hashCode(){ return this.color.hashCode(); }
这里我们首先要明白一个问题:
equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。换句话说,equals()方法不相等的两个对象,hashCode()有可能相等。(我的理解是由于哈希码在生成的时候产生冲突造成的)
在这里hashCode就好比字典里每个字的索引,equals()好比比较的是字典里同一个字下的不同词语。就好像在字典里查“自”这个字下的两个词语“自己”、“自发”,如果用equals()判断查询的词语相等那么就是同一个词语,比如equals()比较的两个词语都是“自己”,那么此时hashCode()方法得到的值也肯定相等;如果用equals()方法比较的是“自己”和“自发”这两个词语,那么得到结果是不想等,但是这两个词都属于“自”这个字下的词语所以在查索引时相同,即:hashCode()相同。如果用equals()比较的是“自己”和“他们”这两个词语的话那么得到的结果也是不同的,此时hashCode() 得到也是不同的。
反过来:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object类中,hashcode()方法是本地方法,返回的是对象的地址值,而object类中的equals()方法比较的也是两个对象的地址值,如果equals()相等,说明两个对象地址值也相等,当然hashcode() 也就相等了;
其实我们也可以看一下8种基本数据类型对应的对象类型和String类型的hashCode方法和equals方法。其中8种基本类型的hashCode很简单就是直接返回他们的数值大小,String对象是通过一个复杂的计算方式,但是这种计算方式能够保证,如果这个字符串的值相等的话,他们的hashCode就是相等的。8种基本类型的equals方法就是直接比较数值,String类型的equals方法是比较字符串的值的。
一般来讲,equals这个方法是给用户调用的,如果你想判断2个对象是否相等,你可以重写equals方法,然后在代码中调用,就可以判断他们是否相等 了。简单来讲,equals方法主要是用来判断从表面上看或者从内容上看,2个对象是不是相等。举个例子,有个学生类,属性只有姓名和性别,那么我们可以 认为只要姓名和性别相等,那么就说这2个对象是相等的。 hashcode方法一般用户不会去调用,比如在hashmap中,由于key是不可以重复的,他在判断key是不是重复的时候就判断了hashcode 这个方法,而且也用到了equals方法。这里不可以重复是说equals和hashcode只要有一个不等就可以了!所以简单来讲,hashcode相当于是一个对象的编码,就好像文件中的md5,他和equals不同就在于他返回的是int型的,比较起来不直观。我们一般在覆盖equals的同时也要 覆盖hashcode,让他们的逻辑一致。举个例子,还是刚刚的例子,如果姓名和性别相等就算2个对象相等的话,那么hashcode的方法也要返回姓名 的hashcode值加上性别的hashcode值,这样从逻辑上,他们就一致了。 要从物理上判断2个对象是否相等,用==就可以了。
hash算法对于查找元素提供了很高的效率
如果想查找一个集合中是否包含有某个对象,大概的程序代码怎样写呢?
你通常是逐一取出每个元素与要查找的对象进行比较,当发现某个元素与要查找的对象进行equals方法比较的结果相等时,则停止继续查找并返回肯定的信息,否则,返回否定的信息,如果一个集合中有很多个元素,比如有一万个元素,并且没有包含要查找的对象时,则意味着你的程序需要从集合中取出一万个元素进行逐一比较才能得到结论。
有人发明了一种哈希算法来提高从集合中查找元素的效率,这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组(使用不同的hash函数来计算的),每组分别对应某个存储区域,根据一个对象的哈希吗就可以确定该对象应该存储在哪个区域HashSet就是采用哈希算法存取对象的集合,它内部采用对某个数字n进行取余(这种的hash函数是最简单的)的方式对哈希码进行分组和划分对象的存储区域;Object类中定义了一个hashCode()方法来返回每个Java对象的哈希码,当从HashSet集合中查找某个对象时,Java系统首先调用对象的hashCode()方法获得该对象的哈希码表,然后根据哈希吗找到相应的存储区域,最后取得该存储区域内的每个元素与该对象进行equals方法比较;这样就不用遍历集合中的所有元素就可以得到结论,可见,HashSet集合具有很好的对象检索性能,但是,HashSet集合存储对象的效率相对要低些,因为向HashSet集合中添加一个对象时,要先计算出对象的哈希码和根据这个哈希码确定对象在集合中的存放位置为了保证一个类的实例对象能在HashSet正常存储,要求这个类的两个实例对象用equals()方法比较的结果相等时,他们的哈希码也必须相等;也就是说,如果obj1.equals(obj2)的结果为true,那么以下表达式的结果也要为true:
obj1.hashCode() == obj2.hashCode()
换句话说:当我们重写一个对象的equals方法,就必须重写他的hashCode方法,不过不重写他的hashCode方法的话,Object对象中的hashCode方法始终返回的是一个对象的hash地址,而这个地址是永远不相等的。所以这时候即使是重写了equals方法,也不会有特定的效果的,因为hashCode方法如果都不想等的话,就不会调用equals方法进行比较了,所以没有意义了。
如果一个类的hashCode()方法没有遵循上述要求,那么,当这个类的两个实例对象用equals()方法比较的结果相等时,他们本来应该无法被同时存储进set集合中,但是,如果将他们存储进HashSet集合中时,由于他们的hashCode()方法的返回值不同(Object中的hashCode方法返回值是永远不同的),第二个对象首先按照哈希码计算可能被放进与第一个对象不同的区域中,这样,它就不可能与第一个对象进行equals方法比较了,也就可能被存储进HashSet集合中了,Object类中的hashCode()方法不能满足对象被存入到HashSet中的要求,因为它的返回值是通过对象的内存地址推算出来的,同一个对象在程序运行期间的任何时候返回的哈希值都是始终不变的,所以,只要是两个不同的实例对象,即使他们的equals方法比较结果相等,他们默认的hashCode方法的返回值是不同的。
equals比较内容是否相等,==判断是否是同一个对象。
==默认比较对象在JVM中的地址。
hashCode 默认返回对象在JVM中的存储地址。
equal比较对象,默认也是比较对象在JVM中的地址,同==
在判断两个对象是否相等时,不要只使用equals方法判断。还要考虑其哈希码是否相等。尤其是和hashMap等与hash相关的数据结构一起使用时。
与jdk7 中switch语法相关
switch中只能使用整型,比如
方法返回的是byte
,short
,char
(ackii码是整型)以及int
。hashCode()int类型,而不是long。jdk7中switch支持string类型,
进行switch
的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。因为Java编译器只增加了一个equals
方法,如果比较的是字符串字面量的话会非常快,比如”abc” ==”abc”。如果把hashCode()
方法的调用也考虑进来了,那么还会再多一次的调用开销,因为字符串一旦创建了,它就会把哈希值缓存起来。因此如果这个siwtch
语句是用在一个循环里的,比如逐项处理某个值,或者游戏引擎循环地渲染屏幕,这里hashCode()
方法的调用开销其实不会很大。
其实swich只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后在使用switch的。
1)对char类型进行比较的时候,实际上比较的是ascii码,编译器会把char型变量转换成对应的int型变量
2)switch对int的判断是直接比较整数的值
3)字符串的switch是通过equals()
和hashCode()
方法来实现的
public class switchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; case "world": System.out.println("world"); break; default: break; } } }
代码反编译后结果:
public class switchDemoString { public switchDemoString() { } public static void main(String args[]) { String str = "world"; String s; switch((s = str).hashCode()) { default: break; case 99162322: if(s.equals("hello")) System.out.println("hello"); break; case 113318802: if(s.equals("world")) System.out.println("world"); break; } } }
参考资料
java中的==、equals()、hashCode()源码分析