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》