Java equals()和hashCode()

一、前言 Java技术面试的时候我们总会被问到这类的问题:重写equals()方法为什么一定要重写hashCode()方法?两个不相等的对象可以有相同的散列码吗?… 曾经对这些问题我也感到很困惑。equals()和hasCode()方法是Object类中的两个基本方法。在实际的应用程序中这两个方法起到了很重要的作用,比如在集合中查找元素,我们经常会根据实际需要重写这两个方法。下面就对equals()方法和hashCode()方法做一个详细的分析说明,希望对于有同样疑惑的人有些许帮助。二、重写equals()方法

1、为什么要重写equals()方法

我们都知道比较两个对象引用是否相同用==运算符,且只有当两个引用都引用相同对象时,使用==运算符才会返回true。而比较相同类型的两个不同对象的内容是否相同,可以使用equals()方法。但是Object中的equals()方法只使用==运算符进行比较,其源码如下:

1publicbooleanequals(Object obj) { 2return(this== obj); 3}

如果我们使用Object中的equals()方法判断相同类型的两个不同对象是否相等,则只有当两个对象引用都引用同一对象时才被视为相等。那么就存在这样一个问题:如果我们要在集合中查找该对象, 在我们不重写equals()方法的情况下,除非我们仍然持有这个对象的引用,否则我们永远找不到相等对象。

代码清单-1

1List<String> test =newArrayList<String>(); 2test.add("aaa"); 3test.add("bbb"); 4System.out.println(test.contains("bbb"));

分析:ArrayList遍历它所有的元素并执行"bbb".equals(element)来判断元素是否和参数对象"bbb"相等。最终是由String类中重写的equals()方法来判断两个字符串是否相等。 2、怎样实现正确的equals()方法

首先,我们需要遵守Java API文档中equals()方法的约定,如下:

自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。

对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。

传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。

一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。

对于任何非空引用值 x,x.equals(null) 都应返回 false。

其次,当我们重写equals()方法时, 不同类型的属性比较方式不同,如下:

属性是Object类型, 包括集合: 使用equals()方法。

属性是类型安全的枚举: 使用equals()方法或==运算符(在这种情况下,它们是相同的)。

属性是可能为空的Object类型: 使用==运算符和equals()方法。

属性是数组类型: 使用Arrays.equals()方法。

属性是除float和double之外的基本类型: 使用==运算符。

属性是float: 使用Float.floatToIntBits方法转化成int,然后使用==运算符。

属性是double: 使用Double.doubleToLongBits方法转化成long , 然后使用==运算符。

值得注意的是,如果属性是基本类型的包装器类型(Integer,Boolean等等), 那么equals方法的实现就会简单一些,因为只需要递归调用equals()方法。

在equals()方法中,通常先执行最重要属性的比较,即最有可能不同的属性先进行比较。可以使用短路运算符&&来最小化执行时间。

3、一个简单的Demo

代码清单-2

01/** 02* 根据上面的策略写的一个工具类 03* 04*/ 05publicfinalclassEqualsUtil { 06 07publicstaticbooleanareEqual(booleanaThis,booleanaThat) { 08returnaThis == aThat; 09} 10 11publicstaticbooleanareEqual(charaThis,charaThat) { 12returnaThis == aThat; 13} 14 15publicstaticbooleanareEqual(longaThis,longaThat) { 16//注意byte, short, 和 int 可以通过隐式转换被这个方法处理 17returnaThis == aThat; 18} 19 20publicstaticbooleanareEqual(floataThis,floataThat) { 21returnFloat.floatToIntBits(aThis) == Float.floatToIntBits(aThat); 22} 23 24publicstaticbooleanareEqual(doubleaThis,doubleaThat) { 25returnDouble.doubleToLongBits(aThis) == Double.doubleToLongBits(aThat); 26} 27 28/** 29* 可能为空的对象属性 30* 包括类型安全的枚举和集合, 但是不包含数组 31*/ 32publicstaticbooleanareEqual(Object aThis, Object aThat) { 33returnaThis ==null? aThat ==null: aThis.equals(aThat); 34} 35}

Car类使用EqualsUtil来实现其equals()方法.

代码清单-3

001importjava.util.ArrayList; 002importjava.util.Arrays; 003importjava.util.Date; 004importjava.util.List; 005 006publicfinalclassCar { 007 008privateString fName; 009privateintfNumDoors; 010privateList<String> fOptions; 011privatedoublefGasMileage; 012privateString fColor; 013privateDate[] fMaintenanceChecks; 014 015publicCar(String aName,intaNumDoors, List<String> aOptions,doubleaGasMileage, String aColor, Date[] aMaintenanceChecks) { 016fName = aName; 017fNumDoors = aNumDoors; 018fOptions =newArrayList<String>(aOptions); 019fGasMileage = aGasMileage; 020fColor = aColor; 021fMaintenanceChecks =newDate[aMaintenanceChecks.length]; 022for(intidx =0; idx < aMaintenanceChecks.length; ++idx) { 023fMaintenanceChecks[idx] =newDate(aMaintenanceChecks[idx].getTime()); 024} 025} 026 027@Override 028publicbooleanequals(Object aThat) { 029//检查自身 030if(this== aThat)returntrue; 031 032//这里使用instanceof 而不是getClass有两个原因 033//1. 如果需要的话, 它可以匹配任何超类型,而不仅仅是一个类; 034//2. 它避免了冗余的校验"that == null" , 因为它已经检查了null - "null instanceof [type]" 总是返回false 035if(!(aThatinstanceofCar))returnfalse; 036//上面一行的另一种写法 : 037//if ( aThat == null || aThat.getClass() != this.getClass() ) return false; 038 039//现在转换成本地对象是安全的(不会抛出ClassCastException) 040Car that = (Car) aThat; 041 042//逐个属性的比较 043return 044EqualsUtil.areEqual(this.fName, that.fName) && 045EqualsUtil.areEqual(this.fNumDoors, that.fNumDoors) && 046EqualsUtil.areEqual(this.fOptions, that.fOptions) && 047EqualsUtil.areEqual(this.fGasMileage, that.fGasMileage) && 048EqualsUtil.areEqual(this.fColor, that.fColor) && 049Arrays.equals(this.fMaintenanceChecks, that.fMaintenanceChecks); 050} 051 052/** 053* 测试equals()方法. 054*/ 055publicstaticvoidmain(String... aArguments) { 056List<String> options =newArrayList<String>(); 057options.add("sunroof"); 058Date[] dates =newDate[1]; 059dates[0] =newDate(); 060 061//创建一堆Car对象,仅有one和two应该是相等的 062Car one =newCar("Nissan",2, options,46.3,"Green", dates); 063 064//two和one相等 065Car two =newCar("Nissan",2, options,46.3,"Green", dates); 066 067//three仅有fName不同 068Car three =newCar("Pontiac",2, options,46.3,"Green", dates); 069 070//four 仅有fNumDoors不同 071Car four =newCar("Nissan",4, options,46.3,"Green", dates); 072 073//five仅有fOptions不同 074List<String> optionsTwo =newArrayList<String>(); 075optionsTwo.add("air conditioning"); 076Car five =newCar("Nissan",2, optionsTwo,46.3,"Green", dates); 077 078//six仅有fGasMileage不同 079Car six =newCar("Nissan",2, options,22.1,"Green", dates); 080 081//seven仅有fColor不同 082Car seven =newCar("Nissan",2, options,46.3,"Fuchsia", dates); 083 084//eight仅有fMaintenanceChecks不同 085Date[] datesTwo =newDate[1]; 086datesTwo[0] =newDate(1000000); 087Car eight =newCar("Nissan",2, options,46.3,"Green", datesTwo); 088 089System.out.println("one = one: "+ one.equals(one)); 090System.out.println("one = two: "+ one.equals(two)); 091System.out.println("two = one: "+ two.equals(one)); 092System.out.println("one = three: "+ one.equals(three)); 093System.out.println("one = four: "+ one.equals(four)); 094System.out.println("one = five: "+ one.equals(five)); 095System.out.println("one = six: "+ one.equals(six)); 096System.out.println("one = seven: "+ one.equals(seven)); 097System.out.println("one = eight: "+ one.equals(eight)); 098System.out.println("one = null: "+ one.equals(null)); 099} 100}

输出结果如下:

01one = one:true 02one = two:true 03two = one:true 04one = three:false 05one = four:false 06one = five:false 07one = six:false 08one = seven:false 09one = eight:false 10one =null:false

三、重写hashCode()方法 1、为什么要重写hashCode()方法

在每个重写了equals()方法的类中也必须要重写hashCode()方法,如果不这样做就会违反Java API中Object类的hashCode()方法的约定,从而导致该类无法很好的用于基于散列的数据结构(HashSet、HashMap、Hashtable、LinkedHashSet、LinkedHashMap等等)。

下面是约定内容:

在 Java 应用程序执行期间,如果没有修改对象的equals()方法的比较操作所用到的信息,那么无论什么时候在同一对象上多次调用 hashCode 方法时,必须一致地返回同一个整数。同一应用程序的多次执行过程中,每次返回的整数可以不一致。

如果两个对象根据 equals(Object) 方法进行比较是相等的,那么调用这两个对象中任意一个对象的hashCode() 方法都必须产生相同的整数结果。

如果两个对象根据 equals(java.lang.Object) 方法进行比较是不相等的,那么调用这两个对象中任意一个对象的hashCode() 方法则不一定要产生不同的整数结果。但是,程序员应该知道,为不相等的对象产生不同整数结果可能会提高哈希表的性能。

因没有重写hashCode()方法而违反的约定是第二条:相等的对象必须具有相同的散列码。

我们来看看Object类中的hashCode()方法: public native int hashCode()。它默认总是为每个不同的对象产生不同的整数结果。即使我们重写equals()方法让类的两个截然不同的实例是相等的,但是根据Object.hashCode()方法,它们是完全不同的两个对象,即如果对象的散列码不能反映它们相等,那么对象怎么相等也没用。

下面是一段测试代码:

代码清单-4

01publicclassEqualsAndHashcode { 02staticclassPerson { 03privateString name; 04privateInteger age; 05publicPerson(String name, Integer age) { 06this.name = name; 07this.age = age; 08} 09@Override 10publicbooleanequals(Object o) { 11if(this== o)returntrue; 12if(!(oinstanceofPerson))returnfalse; 13Person person = (Person) o; 14if(!name.equals(person.name))returnfalse; 15returntrue; 16} 17} 18publicstaticvoidmain(String[] args) { 19Map<Person, String> map =newHashMap<Person, String>(); 20Person person1 =newPerson("aaa",22); 21map.put(person1,"aaa"); 22Person person2 =newPerson("aaa",11); 23System.out.println(person1.equals(person2)); 24System.out.println(map.get(person2)); 25} 26}

输出结果如下:

1true 2null

2、具有相同散列码的对象一定相等吗?

如果散列码不同,元素就会被放入集合中不同的bucket(桶)中。在实际的哈希算法中,在同一个桶(Bucket)内有多个元素的情形非常普遍,因为它们的散列码相同。这时候哈希检索就是一个两步的过程:

1) 使用hashCode()找到正确的桶(bucket)。 2) 使用equals()在桶内找到正确的元素。 所以除非使用equals()方法比较是相等的,否则相同散列码的对象还是不相等。

因此为了定位一个对象,查找对象和集合内的对象二者都必须具有相同的散列码,并且equals()方法也返回true。所以重写equals()方法也必须要重写hashCode()方法才能保证对象可以用作基于散列的集合。

3、如何实现性能好的hashCode()方法

无论所有实例变量是否相等都为其返回相同值,这样的hashCode()仍然是合法的,也是适当的。

代码清单-5

1Override 2publicinthashCode() { 3return1492; 4}

它虽然不违反hashCode()方法的约定,但是它非常低效,因为所有的对象都放在一个bucket内,还是要通过equals()方法费力的找到正确的对象。

一个好的hashCode()方法通常倾向于“为不相等的对象产生不相等的散列码”。这正是hashCode()方法的第三条约定的含义。理想情况下,hashCode()方法应该产生均匀分布的散列码,将不相等的对象均匀分布到所有可能的散列值上。如果散列码都集中在一块儿,那么基于散列的集合在某些bucket的负载会很重。

在《Effective Java》一书中,Joshua Bloch对于怎样写出好的hashCode()方法给出了很好的指导,如下:

1、把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中。

2、对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:

a.为该域计算int类型的散列码c: i.如果该域是boolean类型,则计算(f?1:0)。 ii.如果该域是byte、char、short或者int类型,则计算(int)f。 iii.如果该域是long类型,则计算(int)(f ^ (f >>> 32))。 iv.如果该域是float类型,则计算Float.floatToIntBits(f)。 v.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值。 vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来 比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则 为这个域计算一个“范式(canonical representation)”,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0 (或者其他某个常数,但通常是0)。 vii. 如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。

b.按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:result *31* result+c; 3、返回result。

4、写完了hashCode方法之后,问问自己“相等的实例是否都具有相等的散列码”。要编写单元测试来验证你的推断。如果相等的实例有着不相等的散列码,则要找出原因,并修正错误。

下面是遵循这些指导的一个代码示例

代码清单-6

01publicclassCountedString { 02 03privatestaticList<String> created =newArrayList<String>(); 04privateString s; 05privateintid =0; 06publicCountedString(String str) { 07this.s = str; 08created.add(str); 09for(String s2 : created) { 10if(s2.equals(s)) { 11id++; 12} 13} 14} 15 16@Override 17publicString toString() { 18return"String: "+ s +", id="+ id +" hashCode(): "+ hashCode(); 19} 20 21@Override 22publicbooleanequals(Object o) { 23returnoinstanceofCountedString && s.equals(((CountedString) o).s) && id == ((CountedString) o).id; 24} 25 26@Override 27publicinthashCode() { 28intresult =17; 29result =37* result + s.hashCode(); 30result =37* result + id; 31returnresult; 32} 33 34publicstaticvoidmain(String[] args) { 35Map<CountedString, Integer> map =newHashMap<CountedString, Integer>(); 36CountedString[] cs =newCountedString[5]; 37for(inti =0; i < cs.length; i++) { 38cs[i] =newCountedString("hi"); 39map.put(cs[i], i); 40} 41System.out.println(map); 42for(CountedString cstring : cs) { 43System.out.println("Looking up "+ cstring); 44System.out.println(map.get(cstring)); 45} 46} 47}

输出结果如下:

01{String: hi, id=1hashCode():146447=0, String: hi, id=2hashCode():146448=1, String: hi, id=3hashCode():146449=2, String: hi, id=4hashCode():146450=3, String: hi, id=5hashCode():146451=4} 02Looking up String: hi, id=1hashCode():146447 030 04Looking up String: hi, id=2hashCode():146448 051 06Looking up String: hi, id=3hashCode():146449 072 08Looking up String: hi, id=4hashCode():146450 093 10Looking up String: hi, id=5hashCode():146451 114

4、一个导致hashCode()方法失败的情形

我们都知道,序列化可保存对象,在以后可以通过反序列化再次得到该对象,但是对于transient变量,我们无法对其进行序列化,如果在hashCode()方法中包含一个transient变量,可能会导致放入集合中的对象无法找到。参见下面这个示例:

代码清单-7

01publicclassSaveMeimplementsSerializable { 02 03transientintx; 04inty; 05publicSaveMe(intx,inty) { 06this.x = x; 07this.y = y; 08} 09 10@Override 11publicbooleanequals(Object o) { 12if(this== o)returntrue; 13if(!(oinstanceofSaveMe))returnfalse; 14SaveMe saveMe = (SaveMe) o; 15if(x != saveMe.x)returnfalse; 16if(y != saveMe.y)returnfalse; 17returntrue; 18} 19 20@Override 21publicinthashCode() { 22returnx ^ y; 23} 24 25@Override 26publicString toString() { 27return"SaveMe{"+"x="+ x +", y="+ y +'}'; 28} 29 30publicstaticvoidmain(String[] args)throwsIOException, ClassNotFoundException { 31SaveMe a =newSaveMe(9,5); 32// 打印对象 33System.out.println(a); 34Map<SaveMe, Integer> map =newHashMap<SaveMe, Integer>(); 35map.put(a,10); 36// 序列化a 37ByteArrayOutputStream baos =newByteArrayOutputStream(); 38ObjectOutputStream oos =newObjectOutputStream(baos); 39oos.writeObject(a); 40oos.flush(); 41// 反序列化a 42ObjectInputStream ois=newObjectInputStream(newByteArrayInputStream(baos.toByteArray())); 43SaveMe b = (SaveMe)ois.readObject(); 44// 打印反序列化后的对象 45System.out.println(b); 46// 使用反序列化后的对象检索对象 47System.out.println(map.get(b)); 48} 49}

输出结果如下:

1SaveMe{x=9, y=5} 2SaveMe{x=0, y=5} 3null

从上面的测试可以知道,对象的transient变量反序列化后具有一个默认值,而不是对象保存(或放入HashMap)时该变量所具有的值。

五、总结

当重写equals()方法时,必须要重写hashCode()方法,特别是当对象用于基于散列的集合中时。

六、参考资料

http://www.ibm.com/developerworks/library/j-jtp05273/

http://www.javapractices.com/topic/TopicAction.do?Id=17

《Effective Java》

《Thinking in Java》

于是,月醉了,夜醉了,我也醉了。

Java equals()和hashCode()

相关文章:

你感兴趣的文章:

标签云: