Java技术面试的时候我们总会被问到这类的问题:重写equals()方法为什么一定要重写hashCode()方法?两个不相等的对象可以有相同的散列码吗?... 曾经对这些问题我也感到很困惑。 equals()和hasCode()方法是Object类中的两个基本方法。在实际的应用程序中这两个方法起到了很重要的作用,比如在集合中查找元素,我们经常会根据实际需要重写这两个方法。 下面就对equals()方法和hashCode()方法做一个详细的分析说明,希望对于有同样疑惑的人有些许帮助。
我们都知道比较两个对象引用是否相同用==运算符,且只有当两个引用都引用相同对象时,使用==运算符才会返回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"));
首先,我们需要遵守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()方法中,通常先执行最重要属性的比较,即最有可能不同的属性先进行比较。可以使用短路运算符&&来最小化执行时间。
代码清单-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
在每个重写了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)内有多个元素的情形非常普遍,因为它们的散列码相同。这时候哈希检索就是一个两步的过程:
因此为了定位一个对象,查找对象和集合内的对象二者都必须具有相同的散列码,并且equals()方法也返回true。所以重写equals()方法也必须要重写hashCode()方法才能保证对象可以用作基于散列的集合。
无论所有实例变量是否相等都为其返回相同值,这样的hashCode()仍然是合法的,也是适当的。
代码清单-5
Override public int hashCode() { return 1492; }
它虽然不违反hashCode()方法的约定,但是它非常低效,因为所有的对象都放在一个bucket内,还是要通过equals()方法费力的找到正确的对象。
一个好的hashCode()方法通常倾向于“为不相等的对象产生不相等的散列码”。这正是hashCode()方法的第三条约定的含义。理想情况下,hashCode()方法应该产生均匀分布的散列码,将不相等的对象均匀分布到所有可能的散列值上。如果散列码都集中在一块儿,那么基于散列的集合在某些bucket的负载会很重。
在《Effective Java》一书中,Joshua Bloch对于怎样写出好的hashCode()方法给出了很好的指导,如下:
1、把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中。
2、对于对象中每个关键域f (指equals方法中涉及的每个域),完成以下步骤:
a. 为该域计算int类型的散列码c: 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
我们都知道,序列化可保存对象,在以后可以通过反序列化再次得到该对象,但是对于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》