Java equals()和hashCode()

一、引言

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

二、重写equals()方法

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

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

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

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

        代码清单-1

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

/**
* 根据上面的策略写的一个工具类
*
*/
public final class EqualsUtil {

    public static boolean areEqual(boolean aThis, boolean aThat) {
        return aThis == aThat;
    }

    public static boolean areEqual(char aThis, char aThat) {
        return aThis == aThat;
    }

    public static boolean areEqual(long aThis, long aThat) {
        //注意byte, short, 和 int 可以通过隐式转换被这个方法处理
        return aThis == aThat;
    }

    public static boolean areEqual(float aThis, float aThat) {
        return Float.floatToIntBits(aThis) == Float.floatToIntBits(aThat);
    }

    public static boolean areEqual(double aThis, double aThat) {
        return Double.doubleToLongBits(aThis) == Double.doubleToLongBits(aThat);
    }

    /**
     * 可能为空的对象属性
     * 包括类型安全的枚举和集合, 但是不包含数组
     */
    public static boolean areEqual(Object aThis, Object aThat) {
        return aThis == null ? aThat == null : aThis.equals(aThat);
    }
}
          Car 类使用 EqualsUtil 来实现其 equals ()方法 .

      代码清单-3

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

public final class Car {

    private String fName;
    private int fNumDoors;
    private List<String> fOptions;
    private double fGasMileage;
    private String fColor;
    private Date[] fMaintenanceChecks;

    public Car(String aName, int aNumDoors, List<String> aOptions, double aGasMileage, String aColor, Date[] aMaintenanceChecks) {
        fName = aName;
        fNumDoors = aNumDoors;
        fOptions = new ArrayList<String>(aOptions);
        fGasMileage = aGasMileage;
        fColor = aColor;
        fMaintenanceChecks = new Date[aMaintenanceChecks.length];
        for (int idx = 0; idx < aMaintenanceChecks.length; ++idx) {
            fMaintenanceChecks[idx] = new Date(aMaintenanceChecks[idx].getTime());
        }
    }

    @Override
    public boolean equals(Object aThat) {
        //检查自身
        if (this == aThat) return true;

        //这里使用instanceof 而不是getClass有两个原因
        //1. 如果需要的话, 它可以匹配任何超类型,而不仅仅是一个类;
        //2. 它避免了冗余的校验"that == null" , 因为它已经检查了null - "null instanceof [type]" 总是返回false
        if (!(aThat instanceof Car)) return false;
        //上面一行的另一种写法 :
        //if ( aThat == null || aThat.getClass() != this.getClass() ) return false;

        //现在转换成本地对象是安全的(不会抛出ClassCastException)
        Car that = (Car) aThat;

        //逐个属性的比较
        return
                EqualsUtil.areEqual(this.fName, that.fName) &&
                        EqualsUtil.areEqual(this.fNumDoors, that.fNumDoors) &&
                        EqualsUtil.areEqual(this.fOptions, that.fOptions) &&
                        EqualsUtil.areEqual(this.fGasMileage, that.fGasMileage) &&
                        EqualsUtil.areEqual(this.fColor, that.fColor) &&
                        Arrays.equals(this.fMaintenanceChecks, that.fMaintenanceChecks);
    }

    /**
     * 测试equals()方法.
     */
    public static void main(String... aArguments) {
        List<String> options = new ArrayList<String>();
        options.add("sunroof");
        Date[] dates = new Date[1];
        dates[0] = new Date();

        //创建一堆Car对象,仅有one和two应该是相等的
        Car one = new Car("Nissan", 2, options, 46.3, "Green", dates);

        //two和one相等
        Car two = new Car("Nissan", 2, options, 46.3, "Green", dates);

        //three仅有fName不同
        Car three = new Car("Pontiac", 2, options, 46.3, "Green", dates);

        //four 仅有fNumDoors不同
        Car four = new Car("Nissan", 4, options, 46.3, "Green", dates);

        //five仅有fOptions不同
        List<String> optionsTwo = new ArrayList<String>();
        optionsTwo.add("air conditioning");
        Car five = new Car("Nissan", 2, optionsTwo, 46.3, "Green", dates);

        //six仅有fGasMileage不同
        Car six = new Car("Nissan", 2, options, 22.1, "Green", dates);

        //seven仅有fColor不同
        Car seven = new Car("Nissan", 2, options, 46.3, "Fuchsia", dates);

        //eight仅有fMaintenanceChecks不同
        Date[] datesTwo = new Date[1];
        datesTwo[0] = new Date(1000000);
        Car eight = new Car("Nissan", 2, options, 46.3, "Green", datesTwo);

        System.out.println("one = one: " + one.equals(one));
        System.out.println("one = two: " + one.equals(two));
        System.out.println("two = one: " + two.equals(one));
        System.out.println("one = three: " + one.equals(three));
        System.out.println("one = four: " + one.equals(four));
        System.out.println("one = five: " + one.equals(five));
        System.out.println("one = six: " + one.equals(six));
        System.out.println("one = seven: " + one.equals(seven));
        System.out.println("one = eight: " + one.equals(eight));
        System.out.println("one = null: " + one.equals(null));
    }
}

       输出结果如下:

one = one: true 
one = two: true 
two = one: true 
one = three: false 
one = four: false 
one = five: false 
one = six: false 
one = seven: false 
one = eight: false 
one = 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

public class EqualsAndHashcode {
    static class Person {
        private String name;
        private Integer age;
        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Person)) return false;
            Person person = (Person) o;
            if (!name.equals(person.name)) return false;
            return true;
        }
    }
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<Person, String>();
        Person person1 = new Person("aaa", 22);
        map.put(person1, "aaa");
        Person person2 = new Person("aaa", 11);
        System.out.println(person1.equals(person2));
        System.out.println(map.get(person2));
    }
}
         输出结果如下:    

true
null  

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

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

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

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

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

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

      代码清单-5

Override
public int hashCode() { 
   return 1492;
}

        它虽然不违反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

public class CountedString {

    private static List<String> created = new ArrayList<String>();
    private String s;
    private int id = 0;
    public CountedString(String str) {
        this.s = str;
        created.add(str);
        for (String s2 : created) {
            if (s2.equals(s)) {
                id++;
            }
        }
    }

    @Override
    public String toString() {
        return "String: " + s + ", id=" + id + " hashCode(): " + hashCode();
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof CountedString && s.equals(((CountedString) o).s) && id == ((CountedString) o).id;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 37 * result + s.hashCode();
        result = 37 * result + id;
        return result;
    }

    public static void main(String[] args) {
        Map<CountedString, Integer> map = new HashMap<CountedString, Integer>();
        CountedString[] cs = new CountedString[5];
        for (int i = 0; i < cs.length; i++) {
            cs[i] = new CountedString("hi");
            map.put(cs[i], i);
        }
        System.out.println(map);
        for (CountedString cstring : cs) {
            System.out.println("Looking up " + cstring);
            System.out.println(map.get(cstring));
        }
    }
}
          输出结果如下:

{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}
Looking up String: hi, id=1 hashCode(): 146447
0
Looking up String: hi, id=2 hashCode(): 146448
1
Looking up String: hi, id=3 hashCode(): 146449
2
Looking up String: hi, id=4 hashCode(): 146450
3
Looking up String: hi, id=5 hashCode(): 146451
4

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

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

        代码清单-7

public class SaveMe implements Serializable {

    transient int x;
    int y;
    public SaveMe(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof SaveMe)) return false;
        SaveMe saveMe = (SaveMe) o;
        if (x != saveMe.x) return false;
        if (y != saveMe.y) return false;
        return true;
    }

    @Override
    public int hashCode() {
        return x ^ y;
    }

    @Override
    public String toString() {
        return "SaveMe{" + "x=" + x + ", y=" + y + '}';
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SaveMe a = new SaveMe(9, 5);
        // 打印对象
        System.out.println(a);
        Map<SaveMe, Integer> map = new HashMap<SaveMe, Integer>();
        map.put(a, 10);
        // 序列化a
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(a);
        oos.flush();
        // 反序列化a
        ObjectInputStream ois= new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        SaveMe b = (SaveMe)ois.readObject();
        // 打印反序列化后的对象
        System.out.println(b);
        // 使用反序列化后的对象检索对象
        System.out.println(map.get(b));
    }
}

      输出结果如下:

SaveMe{x=9, y=5}
SaveMe{x=0, y=5}
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》

你可能感兴趣的:(java,equals,HashCode)