Java equals()和hashCode()

1、引言

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

2、重写equals()方法

      一、为何要重写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()方法来判断两个字符串是否相等

      二、怎样实现正确的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()方法中,一般先执行最重要属性的比较,即最有可能不一样的属性先进行比较。可使用短路运算符&&来最小化执行时间。  数据结构

      三、一个简单的Demo     

        代码清单-2 ide

/**
* 根据上面的策略写的一个工具类
*
*/
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

3、重写hashCode()方法

      一、为何要重写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

      二、具备相同散列码的对象必定相等吗?

      若是散列码不一样,元素就会被放入集合中不一样的bucket(桶)中。在实际的哈希算法中,在同一个桶(Bucket)内有多个元素的情形很是广泛,由于它们的散列码相同。这时候哈希检索就是一个两步的过程:

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

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

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

        不管全部实例变量是否相等都为其返回相同值,这样的hashCode()仍然是合法的,也是适当的。

      代码清单-5

Override
public int hashCode() { 
   return 1492;
}

        它虽然不违反hashCode()方法的约定,可是它很是低效,由于全部的对象都放在一个bucket内,仍是要经过equals()方法费力的找到正确的对象。

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

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

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

       二、对于对象中每一个关键域(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;
        三、返回result

        四、写完了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

      四、一个致使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)时该变量所具备的值。

5、总结

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

6、参考资料

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

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

      《Effective Java》

      《Thinking in Java》

相关文章
相关标签/搜索