Java技术面试的时候咱们总会被问到这类的问题:重写equals()方法为何必定要重写hashCode()方法?两个不相等的对象能够有相同的散列码吗?... 曾经对这些问题我也感到很困惑。 equals()和hasCode()方法是Object类中的两个基本方法。在实际的应用程序中这两个方法起到了很重要的做用,好比在集合中查找元素,咱们常常会根据实际须要重写这两个方法。 下面就对equals()方法和hashCode()方法作一个详细的分析说明,但愿对于有一样疑惑的人有些许帮助。 java
咱们都知道比较两个对象引用是否相同用==运算符,且只有当两个引用都引用相同对象时,使用==运算符才会返回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 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
在每一个重写了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()方法给出了很好的指导,以下:
一、把某个非零的常数值,好比说17,保存在一个名为result的int类型的变量中。
二、对于对象中每一个关键域f (指equals方法中涉及的每一个域),完成如下步骤:
a. 为该域计算int类型的散列码c: 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
咱们都知道,序列化可保存对象,在之后能够经过反序列化再次获得该对象,可是对于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》