Java equals()和hashCode()

一、前言

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

二、重写equals()方法

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

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

1 public boolean equals(Object obj) {
2    return (this == obj);
3 }

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

    代码清单-1

1 List<String> test = new ArrayList<String>();
2 test.add("aaa");
3 test.add("bbb");
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

01 /**
02 * 根据上面的策略写的一个工具类
03 *
04 */
05 public final class EqualsUtil {
06
07     public static boolean areEqual(boolean aThis, boolean aThat) {
08         return aThis == aThat;
09     }
10
11     public static boolean areEqual(char aThis, char aThat) {
12         return aThis == aThat;
13     }
14
15     public static boolean areEqual(long aThis, long aThat) {
16         //注意byte, short, 和 int 可以通过隐式转换被这个方法处理
17         return aThis == aThat;
18     }
19
20     public static boolean areEqual(float aThis, float aThat) {
21         return Float.floatToIntBits(aThis) == Float.floatToIntBits(aThat);
22     }
23
24     public static boolean areEqual(double aThis, double aThat) {
25         return Double.doubleToLongBits(aThis) == Double.doubleToLongBits(aThat);
26     }
27
28     /**
29      * 可能为空的对象属性
30      * 包括类型安全的枚举和集合, 但是不包含数组
31      */
32     public static boolean areEqual(Object aThis, Object aThat) {
33         return aThis == null ? aThat == null : aThis.equals(aThat);
34     }
35 }
          Car 类使用 EqualsUtil 来实现其 equals ()方法 .

      代码清单-3

001 import java.util.ArrayList;
002 import java.util.Arrays;
003 import java.util.Date;
004 import java.util.List;
005
006 public final class Car {
007
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;
014
015     public Car(String aName, int aNumDoors, List<String> aOptions, double aGasMileage, String aColor, Date[] aMaintenanceChecks) {
016         fName = aName;
017         fNumDoors = aNumDoors;
018         fOptions = new ArrayList<String>(aOptions);
019         fGasMileage = aGasMileage;
020         fColor = aColor;
021         fMaintenanceChecks = new Date[aMaintenanceChecks.length];
022         for (int idx = 0; idx < aMaintenanceChecks.length; ++idx) {
023             fMaintenanceChecks[idx] = new Date(aMaintenanceChecks[idx].getTime());
024         }
025     }
026
027     @Override
028     public boolean equals(Object aThat) {
029         //检查自身
030         if (this == aThat) return true;
031
032         //这里使用instanceof 而不是getClass有两个原因
033         //1. 如果需要的话, 它可以匹配任何超类型,而不仅仅是一个类;
034         //2. 它避免了冗余的校验"that == null" , 因为它已经检查了null - "null instanceof [type]" 总是返回false
035         if (!(aThat instanceof Car)) return false;
036         //上面一行的另一种写法 :
037         //if ( aThat == null || aThat.getClass() != this.getClass() ) return false;
038
039         //现在转换成本地对象是安全的(不会抛出ClassCastException)
040         Car that = (Car) aThat;
041
042         //逐个属性的比较
043         return
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);
050     }
051
052     /**
053      * 测试equals()方法.
054      */
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();
060
061         //创建一堆Car对象,仅有one和two应该是相等的
062         Car one = new Car("Nissan"2, options, 46.3"Green", dates);
063
064         //two和one相等
065         Car two = new Car("Nissan"2, options, 46.3"Green", dates);
066
067         //three仅有fName不同
068         Car three = new Car("Pontiac"2, options, 46.3"Green", dates);
069
070         //four 仅有fNumDoors不同
071         Car four = new Car("Nissan"4, options, 46.3"Green", dates);
072
073         //five仅有fOptions不同
074         List<String> optionsTwo = new ArrayList<String>();
075         optionsTwo.add("air conditioning");
076         Car five = new Car("Nissan"2, optionsTwo, 46.3"Green", dates);
077
078         //six仅有fGasMileage不同
079         Car six = new Car("Nissan"2, options, 22.1"Green", dates);
080
081         //seven仅有fColor不同
082         Car seven = new Car("Nissan"2, options, 46.3"Fuchsia", dates);
083
084         //eight仅有fMaintenanceChecks不同
085         Date[] datesTwo = new Date[1];
086         datesTwo[0] = new Date(1000000);
087         Car eight = new Car("Nissan"2, options, 46.3"Green", datesTwo);
088
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));
099     }
100 }

       输出结果如下:

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

三、重写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 {
02     static class Person {
03         private String name;
04         private Integer age;
05         public Person(String name, Integer age) {
06             this.name = name;
07             this.age = age;
08         }
09         @Override
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;
15             return true;
16         }
17     }
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));
25     }
26 }
         输出结果如下:    

1 true
2 null

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

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

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

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

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

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

    代码清单-5

1 Override
2 public int hashCode() {
3    return 1492;
4 }

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

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

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

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

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

       a.为该域计算int类型的散列码c:
         i.如果该域是boolean类型,则计算(f?1:0)
         ii.如果该域是bytecharshort或者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 {
02
03     private static List<String> created = new ArrayList<String>();
04     private String s;
05     private int id = 0;
06     public CountedString(String str) {
07         this.s = str;
08         created.add(str);
09         for (String s2 : created) {
10             if (s2.equals(s)) {
11                 id++;
12             }
13         }
14     }
15
16     @Override
17     public String toString() {
18         return "String: " + s + ", id=" + id + " hashCode(): " + hashCode();
19     }
20
21     @Override
22     public boolean equals(Object o) {
23         return instanceof CountedString && s.equals(((CountedString) o).s) && id == ((CountedString) o).id;
24     }
25
26     @Override
27     public int hashCode() {
28         int result = 17;
29         result = 37 * result + s.hashCode();
30         result = 37 * result + id;
31         return result;
32     }
33
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");
39             map.put(cs[i], i);
40         }
41         System.out.println(map);
42         for (CountedString cstring : cs) {
43             System.out.println("Looking up " + cstring);
44             System.out.println(map.get(cstring));
45         }
46     }
47 }
          输出结果如下:

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
03 0
04 Looking up String: hi, id=2 hashCode(): 146448
05 1
06 Looking up String: hi, id=3 hashCode(): 146449
07 2
08 Looking up String: hi, id=4 hashCode(): 146450
09 3
10 Looking up String: hi, id=5 hashCode(): 146451
11 4

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

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

    代码清单-7

01 public class SaveMe implements Serializable {
02
03     transient int x;
04     int y;
05     public SaveMe(int x, int y) {
06         this.x = x;
07         this.y = y;
08     }
09
10     @Override
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;
17         return true;
18     }
19
20     @Override
21     public int hashCode() {
22         return x ^ y;
23     }
24
25     @Override
26     public String toString() {
27         return "SaveMe{" "x=" + x + ", y=" + y + '}';
28     }
29
30     public static void main(String[] args) throws IOException, ClassNotFoundException {
31         SaveMe a = new SaveMe(95);
32         // 打印对象
33         System.out.println(a);
34         Map<SaveMe, Integer> map = new HashMap<SaveMe, Integer>();
35         map.put(a, 10);
36         // 序列化a
37         ByteArrayOutputStream baos = new ByteArrayOutputStream();
38         ObjectOutputStream oos = new ObjectOutputStream(baos);
39         oos.writeObject(a);
40         oos.flush();
41         // 反序列化a
42         ObjectInputStream ois= new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
43         SaveMe b = (SaveMe)ois.readObject();
44         // 打印反序列化后的对象
45         System.out.println(b);
46         // 使用反序列化后的对象检索对象
47         System.out.println(map.get(b));
48     }
49 }

      输出结果如下:

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

    从上面的测试可以知道,对象的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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值