Java的equals方法實現及其細節


  判斷兩個對象是否等價,是OOP編程中常見的需求(下面圍繞Java來進行闡述)。

  考慮這樣幾種情況:通過某個特征值來判斷兩個對象是否“等價”,當這兩個對象等價時,判斷結果為true,否則結果為false。

  當然,這里的“特征值”不會只是簡單的“對象引用”,事實上,Object類(Java的“對象世界”的根)中實現的equals方法,就是把“特征值”設定為“對象引用”來進行判斷等價性的,因此可以得知,Object類中equals方法只是簡簡單單地返回this引用和被判斷的obj的引用的“==運算”的值。

  但是很多情況下,並不是要求兩個對象只有引用相同時(此時二者為一個對象)才“判定為等價”,這就需要ADT設計者來界定兩個實例對象判斷等價的條件,即設定要比較的特征值。

  舉個例子,在某個社交軟件中,要求每個用戶的用戶名(name)必須獨一無二,那么在每次增加新用戶的時候,都要對該用戶的注冊名進行判斷,如果當前用戶名已經被占用,則無法為該用戶創建賬號,只能要求該新用戶重新選擇設定用戶名。

  這個時候,可以確定,判斷兩個對象是否等價的特征值就是name(不妨把它設定為一個String類型的私有屬性),這里對這個實現過程進行模擬。

  這里說明一下equals內代碼的個人寫法:

    第一步,先判斷引用值是否相等,此時person1.equals(person1)這樣的情況,就可以很快返回結果true。

    第二步,判斷類型是否匹配,如果兩個對象等價,前提是它們一定為相同的類型,此時person1.equals(null)這樣的情況,也能進行判斷並返回結果false。

    第三步,按部就班地按照預設的特征值進行對象的等價性判斷。

  運行結果:

  這里說明幾點:

  1.類中的equals方法是一定要重寫/覆蓋(Override)的,因為要讓它按照設計的需求來根據特征值判斷等價性。

    這里的特征值,就是String類型的name屬性,表示每個Person對象的名字。由於在equals方法中只設定了這一個需要比較的特征值,因此只要兩個Person類對象的name相同,那么他們的判斷結果就是相同。

  2.類中的hashCode方法需要重寫/覆蓋

    事實上,當實現了1之后,就能保證判斷兩個對象等價性是否成立了(此時已經能保證程序中person1.equals(person2)值為true。但是這樣得到的equals方法是有很大限定性的。比如把person1加入到一個HashSet中,此時判斷HashSet中是否包含person2,由於在設計時,特征值只是name,那么此時期望HashSet.contains(person2)的值也應為true,但如果不實現hashCode方法,返回值只能是false。

    對於這個原因,可以把Java中每個實例對象的存儲過程都想象成“將包含該對象的數據‘拋到’一個桶里”,為了更快地比價,就把整個程序運行時的空間,分成相當多的“桶”,並為每個桶編號,對於桶內裝載的數據,有這樣的規定:為每個實例對象進行編號,只有編號相同的兩個對象,它們才有可能分配到一個桶里。這樣一來,要想判斷兩個對象是否等價(即是否能讓equals方法返回true),只需要訪問這個桶就可以了,因為這兩個對象一定是出現在相同的桶里的。步驟1已經實現了“找到兩個對象之后,根據某個特征值進行判斷”,但是並未實現“讓兩個對象分配到一個桶里”。這就是問題的關鍵所在。所以為了保證兩個對象分配到相同的“桶”里,就要重寫它們的hashCode方法,Java中為每種類型都默認實現了該類型的hashCode方法。下面的實現了hashCode的代碼中,由於特征值是name,為了保證這兩個Person類對象等價,那么它們的name一定相同,那考慮到name(Sting類型)已經實現了hashCode,此時就簡單地把它們的name的hashCode值進行返回即可。這樣就能保證,如果兩個Person對象的name如果相同,那么它們的hashCode一定相同,同時也便於下一步判斷。

    注:重點在於理解這個“桶”的概念,通過這個抽象過程,便也可以很好地理解“Java中兩個等價的對象一定有相同的hashCode值,但兩個擁有相同hashCode值的對象不一定等價”這句話。這句話的重點就在於考慮“桶”是如何裝載的、以及它“裝載”的是什么類型對象等等細節。

  這里給出未實現hashCode的Person類,並展示其測試代碼: 

  測試結果:

  可見,未實現hashCode時,set.contains(person2)為false,即此時HashSet類型在檢索person2時,發現它不在其裝載對象(perosn1)所在的“桶”里,於是直接返回false。

  此時,重新實現代碼:

  此時再次測試上述測試代碼,測試結果:

  可以看到,盡管測試set未裝載person2,但根據重寫的equals判定等價性規則,person2也是被判定符合等價性的,因此在實現了hashCode后,便也能讓持有對象按照設定的規則判斷其等價性。

 

  當然,上述實現代碼以及測試都是基於特征值為name來進行實現的,在現實生活中,比如“居民身份證”來說,判斷兩個對象是否“等價”(即是否為同一個人),特征值自然就包括name(名字),sex(性別),age(年齡)等等屬性,考慮到使用居民身份證的頻繁使用以及廣泛的應用場景,每個居民就理所應當地擁有了一個額外的“屬性”: 身份證號。這個獨一無二的值,既實現了每個對象的區別,又能很方便地進行排序(從而進行檢索等操作)。

  由此可見,現實生活中處處體現着“ADT設計者的智慧”...

  Java為程序開發者提供了靈活的設定“特征值”的方法,因此在設計一種需要的數據類型時,可以仔細地思考一下兩個對象判斷等價的依據(特征值)究竟是什么,這樣實現的equals方法,往往給ADT的使用過程帶來了極大的便利。

 

  from Steven Shen

  編輯於2018.6.19

    修改於2018.6.20


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM