一、前言
Java技术面试的时候我们总会被问到这类的问题:重写equals()方法为什么一定要重写hashCode()方法?两个不相等的对象可以有相同的散列码吗?... 曾经对这些问题我也感到很困惑。equals()和hasCode()方法是Object类中的两个基本方法。在实际的应用程序中这两个方法起到了很重要的作用,比如在集合中查找元素,我们经常会根据实际需要重写这两个方法。 下面就对equals()方法和hashCode()方法做一个详细的分析说明,希望对于有同样疑惑的人有些许帮助。
二、重写equals()方法
1、为什么要重写equals()方法
我们都知道比较两个对象引用是否相同用==运算符,且只有当两个引用都引用相同对象时,使用==运算符才会返回true。而比较相同类型的两个不同对象的内容是否相同,可以使用equals()方法。但是Object中的equals()方法只使用==运算符进行比较,其源码如下:
1 | public boolean equals(Object obj) { |
如果我们使用Object中的equals()方法判断相同类型的两个不同对象是否相等,则只有当两个对象引用都引用同一对象时才被视为相等。那么就存在这样一个问题:如果我们要在集合中查找该对象, 在我们不重写equals()方法的情况下,除非我们仍然持有这个对象的引用,否则我们永远找不到相等对象。
代码清单-1
1 | List<String> test = new ArrayList<String>(); |
4 | System.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
05 | public final class EqualsUtil { |
07 | public static boolean areEqual( boolean aThis, boolean aThat) { |
08 | return aThis == aThat; |
11 | public static boolean areEqual( char aThis, char aThat) { |
12 | return aThis == aThat; |
15 | public static boolean areEqual( long aThis, long aThat) { |
17 | return aThis == aThat; |
20 | public static boolean areEqual( float aThis, float aThat) { |
21 | return Float.floatToIntBits(aThis) == Float.floatToIntBits(aThat); |
24 | public static boolean areEqual( double aThis, double aThat) { |
25 | return Double.doubleToLongBits(aThis) == Double.doubleToLongBits(aThat); |
30 | * 包括类型安全的枚举和集合, 但是不包含数组 |
32 | public static boolean areEqual(Object aThis, Object aThat) { |
33 | return aThis == null ? aThat == null : aThis.equals(aThat); |
Car 类使用 EqualsUtil 来实现其 equals ()方法 .
代码清单-3
001 | import java.util.ArrayList; |
002 | import java.util.Arrays; |
003 | import java.util.Date; |
004 | import java.util.List; |
006 | public final class Car { |
008 | private String fName; |
009 | private int fNumDoors; |
010 | private List<String> fOptions; |
011 | private double fGasMileage; |
012 | private String fColor; |
013 | private Date[] fMaintenanceChecks; |
015 | public Car(String aName, int aNumDoors, List<String> aOptions, double aGasMileage, String aColor, Date[] aMaintenanceChecks) { |
017 | fNumDoors = aNumDoors; |
018 | fOptions = new ArrayList<String>(aOptions); |
019 | fGasMileage = aGasMileage; |
021 | fMaintenanceChecks = new Date[aMaintenanceChecks.length]; |
022 | for ( int idx = 0 ; idx < aMaintenanceChecks.length; ++idx) { |
023 | fMaintenanceChecks[idx] = new Date(aMaintenanceChecks[idx].getTime()); |
028 | public boolean equals(Object aThat) { |
030 | if ( this == aThat) return true ; |
035 | if (!(aThat instanceof Car)) return false ; |
040 | Car that = (Car) aThat; |
044 | EqualsUtil.areEqual( this .fName, that.fName) && |
045 | EqualsUtil.areEqual( this .fNumDoors, that.fNumDoors) && |
046 | EqualsUtil.areEqual( this .fOptions, that.fOptions) && |
047 | EqualsUtil.areEqual( this .fGasMileage, that.fGasMileage) && |
048 | EqualsUtil.areEqual( this .fColor, that.fColor) && |
049 | Arrays.equals( this .fMaintenanceChecks, that.fMaintenanceChecks); |
055 | public static void main(String... aArguments) { |
056 | List<String> options = new ArrayList<String>(); |
057 | options.add( "sunroof" ); |
058 | Date[] dates = new Date[ 1 ]; |
059 | dates[ 0 ] = new Date(); |
062 | Car one = new Car( "Nissan" , 2 , options, 46.3 , "Green" , dates); |
065 | Car two = new Car( "Nissan" , 2 , options, 46.3 , "Green" , dates); |
068 | Car three = new Car( "Pontiac" , 2 , options, 46.3 , "Green" , dates); |
071 | Car four = new Car( "Nissan" , 4 , options, 46.3 , "Green" , dates); |
074 | List<String> optionsTwo = new ArrayList<String>(); |
075 | optionsTwo.add( "air conditioning" ); |
076 | Car five = new Car( "Nissan" , 2 , optionsTwo, 46.3 , "Green" , dates); |
079 | Car six = new Car( "Nissan" , 2 , options, 22.1 , "Green" , dates); |
082 | Car seven = new Car( "Nissan" , 2 , options, 46.3 , "Fuchsia" , dates); |
085 | Date[] datesTwo = new Date[ 1 ]; |
086 | datesTwo[ 0 ] = new Date( 1000000 ); |
087 | Car eight = new Car( "Nissan" , 2 , options, 46.3 , "Green" , datesTwo); |
089 | System.out.println( "one = one: " + one.equals(one)); |
090 | System.out.println( "one = two: " + one.equals(two)); |
091 | System.out.println( "two = one: " + two.equals(one)); |
092 | System.out.println( "one = three: " + one.equals(three)); |
093 | System.out.println( "one = four: " + one.equals(four)); |
094 | System.out.println( "one = five: " + one.equals(five)); |
095 | System.out.println( "one = six: " + one.equals(six)); |
096 | System.out.println( "one = seven: " + one.equals(seven)); |
097 | System.out.println( "one = eight: " + one.equals(eight)); |
098 | System.out.println( "one = null: " + one.equals( null )); |
输出结果如下:
三、重写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
01 | public class EqualsAndHashcode { |
05 | public Person(String name, Integer age) { |
10 | public boolean equals(Object o) { |
11 | if ( this == o) return true ; |
12 | if (!(o instanceof Person)) return false ; |
13 | Person person = (Person) o; |
14 | if (!name.equals(person.name)) return false ; |
18 | public static void main(String[] args) { |
19 | Map<Person, String> map = new HashMap<Person, String>(); |
20 | Person person1 = new Person( "aaa" , 22 ); |
21 | map.put(person1, "aaa" ); |
22 | Person person2 = new Person( "aaa" , 11 ); |
23 | System.out.println(person1.equals(person2)); |
24 | System.out.println(map.get(person2)); |
输出结果如下:
2、具有相同散列码的对象一定相等吗?
如果散列码不同,元素就会被放入集合中不同的bucket(桶)中。在实际的哈希算法中,在同一个桶(Bucket)内有多个元素的情形非常普遍,因为它们的散列码相同。这时候哈希检索就是一个两步的过程:
1) 使用hashCode()找到正确的桶(bucket)。
2) 使用equals()在桶内找到正确的元素。
所以除非使用equals()方法比较是相等的,否则相同散列码的对象还是不相等。
因此为了定位一个对象,查找对象和集合内的对象二者都必须具有相同的散列码,并且equals()方法也返回true。所以重写equals()方法也必须要重写hashCode()方法才能保证对象可以用作基于散列的集合。
3、如何实现性能好的hashCode()方法
无论所有实例变量是否相等都为其返回相同值,这样的hashCode()仍然是合法的,也是适当的。
代码清单-5
2 | public int hashCode() { |
它虽然不违反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
01 | public class CountedString { |
03 | private static List<String> created = new ArrayList<String>(); |
06 | public CountedString(String str) { |
09 | for (String s2 : created) { |
17 | public String toString() { |
18 | return "String: " + s + ", id=" + id + " hashCode(): " + hashCode(); |
22 | public boolean equals(Object o) { |
23 | return o instanceof CountedString && s.equals(((CountedString) o).s) && id == ((CountedString) o).id; |
27 | public int hashCode() { |
29 | result = 37 * result + s.hashCode(); |
30 | result = 37 * result + id; |
34 | public static void main(String[] args) { |
35 | Map<CountedString, Integer> map = new HashMap<CountedString, Integer>(); |
36 | CountedString[] cs = new CountedString[ 5 ]; |
37 | for ( int i = 0 ; i < cs.length; i++) { |
38 | cs[i] = new CountedString( "hi" ); |
41 | System.out.println(map); |
42 | for (CountedString cstring : cs) { |
43 | System.out.println( "Looking up " + cstring); |
44 | System.out.println(map.get(cstring)); |
输出结果如下:
01 | {String: hi, id= 1 hashCode(): 146447 = 0 , String: hi, id= 2 hashCode(): 146448 = 1 , String: hi, id= 3 hashCode(): 146449 = 2 , String: hi, id= 4 hashCode(): 146450 = 3 , String: hi, id= 5 hashCode(): 146451 = 4 } |
02 | Looking up String: hi, id= 1 hashCode(): 146447 |
04 | Looking up String: hi, id= 2 hashCode(): 146448 |
06 | Looking up String: hi, id= 3 hashCode(): 146449 |
08 | Looking up String: hi, id= 4 hashCode(): 146450 |
10 | Looking up String: hi, id= 5 hashCode(): 146451 |
4、一个导致hashCode()方法失败的情形
我们都知道,序列化可保存对象,在以后可以通过反序列化再次得到该对象,但是对于transient变量,我们无法对其进行序列化,如果在hashCode()方法中包含一个transient变量,可能会导致放入集合中的对象无法找到。参见下面这个示例:
代码清单-7
01 | public class SaveMe implements Serializable { |
05 | public SaveMe( int x, int y) { |
11 | public boolean equals(Object o) { |
12 | if ( this == o) return true ; |
13 | if (!(o instanceof SaveMe)) return false ; |
14 | SaveMe saveMe = (SaveMe) o; |
15 | if (x != saveMe.x) return false ; |
16 | if (y != saveMe.y) return false ; |
21 | public int hashCode() { |
26 | public String toString() { |
27 | return "SaveMe{" + "x=" + x + ", y=" + y + '}' ; |
30 | public static void main(String[] args) throws IOException, ClassNotFoundException { |
31 | SaveMe a = new SaveMe( 9 , 5 ); |
33 | System.out.println(a); |
34 | Map<SaveMe, Integer> map = new HashMap<SaveMe, Integer>(); |
37 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
38 | ObjectOutputStream oos = new ObjectOutputStream(baos); |
42 | ObjectInputStream ois= new ObjectInputStream( new ByteArrayInputStream(baos.toByteArray())); |
43 | SaveMe b = (SaveMe)ois.readObject(); |
45 | System.out.println(b); |
47 | System.out.println(map.get(b)); |
输出结果如下:
从上面的测试可以知道,对象的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》
本文转自:http://my.oschina.net/jackieyeah/blog/213573