八、覆蓋equals時(shí)請(qǐng)遵守通用約定:
?? ?? 對(duì)于Object類中提供的equals方法在必要的時(shí)候是必要重載的,然而如果違背了一些通用的重載準(zhǔn)則,將會(huì)給程序帶來(lái)一些潛在的運(yùn)行時(shí)錯(cuò)誤。如果自定義的class沒(méi)有重載該方法,那么該類實(shí)例之間的相等性的比較將是基于兩個(gè)對(duì)象是否指向同一地址來(lái)判定的。因此對(duì)于以下幾種情況可以考慮不重載該方法:
? ? ? 1.?? ?類的每一個(gè)實(shí)例本質(zhì)上都是唯一的。
? ? ? 不同于值對(duì)象,需要根據(jù)其內(nèi)容作出一定的判定,然而該類型的類,其實(shí)例的自身便具備了一定的唯一性,如Thread、Timer等,他本身并不具備更多邏輯比較的必要性。
?? ?? 2.?? ?不關(guān)心類是否提供了“邏輯相等”的測(cè)試功能。
? ? ? 如Random類,開發(fā)者在使用過(guò)程中并不關(guān)心兩個(gè)Random對(duì)象是否可以生成同樣隨機(jī)數(shù)的值,對(duì)于一些工具類亦是如此,如NumberFormat和DateFormat等。
?? ?? 3.?? ?超類已經(jīng)覆蓋了equals,從超類繼承過(guò)來(lái)的行為對(duì)于子類也是合適的。
?? ?? 如Set實(shí)現(xiàn)都從AbstractSet中繼承了equals實(shí)現(xiàn),因此其子類將不在需要重新定義該方法,當(dāng)然這也是充分利用了繼承的一個(gè)優(yōu)勢(shì)。
?? ?? 4.?? ?類是私有的或是包級(jí)別私有的,可以確定它的equals方法永遠(yuǎn)不會(huì)被調(diào)用。
?? ?
?? ?? 那么什么時(shí)候應(yīng)該覆蓋Object.equals呢?如果類具有自己特有的“邏輯相等”概念,而且超類中沒(méi)有覆蓋equals以實(shí)現(xiàn)期望的行為,這是我們就需要覆蓋equals方法,如各種值對(duì)象,或者像Integer和Date這種表示某個(gè)值的對(duì)象。在重載之后,當(dāng)對(duì)象插入Map和Set等容器中時(shí),可以得到預(yù)期的行為。枚舉也可以被視為值對(duì)象,然而卻是這種情形的一個(gè)例外,對(duì)于枚舉是沒(méi)有必要重載equals方法,直接比較對(duì)象地址即可,而且效率也更高。
? ? ? 在覆蓋equals是,該條目給出了通用的重載原則:
?? ?? 1.?? ?自反性:對(duì)于非null的引用值x,x.equals(x)返回true。
? ? ? 如果違反了該原則,當(dāng)x對(duì)象實(shí)例被存入集合之后,下次希望從該集合中取出該對(duì)象時(shí),集合的contains方法將直接無(wú)法找到之前存入的對(duì)象實(shí)例。
? ? ? 2.?? ?對(duì)稱性:對(duì)于任何非null的引用值x和y,如果y.equals(x)為true,那么x.equals(y)也為true。
?? ?? 對(duì)于上例,如果執(zhí)行cis.equals(s)將會(huì)返回true,因?yàn)樵谠揷lass的equals方法中對(duì)參數(shù)o的類型針對(duì)String作了特殊的判斷和特殊的處理,因此如果equals中傳入的參數(shù)類型為String時(shí),可以進(jìn)一步完成大小寫不敏感的比較。然而在String的equals中,并沒(méi)有針對(duì)CaseInsensitiveString類型做任何處理,因此s.equals(cis)將一定返回false。針對(duì)該示例代碼,由于無(wú)法確定List.contains的實(shí)現(xiàn)是基于cis.equals(s)還是基于s.equals(cis),對(duì)于實(shí)現(xiàn)邏輯兩者都是可以接受的,既然如此,外部的使用者在調(diào)用該方法時(shí)也應(yīng)該同樣保證并不依賴于底層的具體實(shí)現(xiàn)邏輯。由此可見,equals方法的對(duì)稱性是非常必要的。以上的equals實(shí)現(xiàn)可以做如下修改:
1 @Override public boolean equals(Object o) { 2 if (o instanceof CaseInsensitiveString) 3 return s.equalsIgnoreCase((CaseInsensitiveString)o).s); 4 return false ; 5 }
? ? ? 這樣修改之后,cis.equals(s)和s.equals(cis)都將返回false。?? ?
? ? ? 3.?? ?傳遞性:對(duì)于任何非null的引用值x、y和z,如果x.equals(y)返回true,同時(shí)y.equals(z)也返回true,那么x.equals(z)也必須返回true。
? ? ? 對(duì)于該類的equals重載是沒(méi)有任何問(wèn)題了,該邏輯可以保證傳遞性,然而在我們?cè)噲D給Point類添加新的子類時(shí),會(huì)是什么樣呢?
? ? ? 如果在ColorPoint中沒(méi)有重載自己的equals方法而是直接繼承自超類,這樣的相等性比較邏輯將會(huì)給使用者帶來(lái)極大的迷惑,畢竟Color域字段對(duì)于ColorPoint而言確實(shí)是非常有意義的比較性字段,因此該類重載了自己的equals方法。然而這樣的重載方式確實(shí)帶來(lái)了一些潛在的問(wèn)題,見如下代碼:
? ? ? 從輸出結(jié)果來(lái)看,ColorPoint.equals方法破壞了相等性規(guī)則中的對(duì)稱性,因此需要做如下修改:
? ? ? 經(jīng)過(guò)這樣的修改,對(duì)稱性確實(shí)得到了保證,但是卻犧牲了傳遞性,見如下代碼:
?? ?? 再次看輸出結(jié)果,傳遞性確實(shí)被打破了。如果我們?cè)赑oint.equals中不使用instanceof而是直接使用getClass呢?
1 @Override public boolean equals(Object o) { 2 if (o == null || o.getClass() == getClass()) 3 return false ; 4 Point p = (Point)o; 5 return p.x == x && p.y == y; 6 }
?? ?? 這樣的Point.equals確實(shí)保證了對(duì)象相等性的這幾條規(guī)則,然而在實(shí)際應(yīng)用中又是什么樣子呢?
? ? ? 如果此時(shí)我們測(cè)試的不是Point類本身,而是ColorPoint,那么按照目前Point.equals(getClass方式)的實(shí)現(xiàn)邏輯,ColorPoint對(duì)象在被傳入onUnitCircle方法后,將永遠(yuǎn)不會(huì)返回true,這樣的行為違反了"里氏替換原則"(敏捷軟件開發(fā)一書中給出了很多的解釋),既一個(gè)類型的任何重要屬性也將適用于它的子類型。因此該類型編寫的任何方法,在它的子類型上也應(yīng)該同樣運(yùn)行的很好。
?? ?? 如何解決這個(gè)問(wèn)題,該條目給出了一個(gè)折中的方案,既復(fù)合優(yōu)先于繼承,見如下代碼:
????? 4.?? ?一致性:對(duì)于任何非null的引用值x和y,只要equals的比較操作在對(duì)象中所用的信息沒(méi)有被改變,多次調(diào)用x.equals(y)就會(huì)一致的返回true,或者一致返回false。
? ? ? 在實(shí)際的編碼中,盡量不要讓類的equals方法依賴一些不確定性較強(qiáng)的域字段,如path。由于path有多種表示方式可以指向相同的目錄,特別是當(dāng)path中包含主機(jī)名稱或ip地址等信息時(shí),更增加了它的不確定性。再有就是path還存在一定的平臺(tái)依賴性。
?? ?? 5.?? ?非空性:很難想象會(huì)存在o.equals(null)返回true的正常邏輯。作為JDK框架中極為重要的方法之一,equals方法被JDK中的基礎(chǔ)類廣泛的使用,因此作為一種通用的約定,像equals、toString、hashCode和compareTo等重要的通用方法,開發(fā)者在重載時(shí)不應(yīng)該讓自己的實(shí)現(xiàn)拋出異常,否則會(huì)引起很多潛在的Bug。如在Map集合中查找指定的鍵,由于查找過(guò)程中的鍵相等性的比較就是利用鍵對(duì)象的equals方法,如果此時(shí)重載后的equals方法拋出NullPointerException異常,而Map的get方法并未捕獲該異常,從而導(dǎo)致系統(tǒng)的運(yùn)行時(shí)崩潰錯(cuò)誤,然而事實(shí)上,這樣的問(wèn)題是完全可以通過(guò)正常的校驗(yàn)手段來(lái)避免的。綜上所述,很多對(duì)象在重載equals方法時(shí)都會(huì)首先對(duì)輸入的參數(shù)進(jìn)行是否為null的判斷,見如下代碼:
????? 注意以上代碼中的instanceof判斷,由于在后面的實(shí)現(xiàn)中需要將參數(shù)o進(jìn)行類型強(qiáng)轉(zhuǎn),如果類型不匹配則會(huì)拋出ClassCastException,導(dǎo)致equals方法提前退出。在此需要指出的是instanceof還有一個(gè)潛在的規(guī)則,如果其左值為null,instanceof操作符將始終返回false,因此上面的代碼可以優(yōu)化為:
1 @Override public boolean equals(Object o) { 2 if (!(o instanceof MyType)) 3 return false ; 4 ... 5 }
? ? ? 鑒于之上所述,該條目中給出了重載equals方法的最佳邏輯:
? ? ? 1.?? ?使用==操作符檢查"參數(shù)是否為這個(gè)對(duì)象的引用",如果是則返回true。由于==操作符是基于對(duì)象地址的比較,因此特別針對(duì)擁有復(fù)雜比較邏輯的對(duì)象而言,這是一種性能優(yōu)化的方式。
? ? ? 2.?? ?使用instanceof操作符檢查"參數(shù)是否為正確的類型",如果不是則返回false。
? ? ? 3.?? ?把參數(shù)轉(zhuǎn)換成為正確的類型。由于已經(jīng)通過(guò)instanceof的測(cè)試,因此不會(huì)拋出ClassCastException異常。
? ? ? 4.?? ?對(duì)于該類中的每個(gè)"關(guān)鍵"域字段,檢查參數(shù)中的域是否與該對(duì)象中對(duì)應(yīng)的域相匹配。
? ? ? 如果以上測(cè)試均全部成功返回true,否則false。見如下示例代碼:
? ? ? 從上面的示例中可以看出,如果域字段為Object對(duì)象,則使用equals方法進(jìn)行兩者之間的相等性比較,如果為int等整型基本類型,可以直接比較,如果為浮點(diǎn)型基本類型,考慮到精度和Double.NaN和Float.NaN等問(wèn)題,推薦使用其對(duì)應(yīng)包裝類的compare方法,如果是數(shù)組,可以使用JDK 1.5中新增的Arrays.equals方法。眾所周知,&&操作符是有短路原則的,因此應(yīng)該將最有可能不相同和比較開銷更低的域比較放在最前面。
? ? ? 最后需要提起注意的是Object.equals的參數(shù)類型為Object,如果要重載該方法,必須保持參數(shù)列表的一致性,如果我們將子類的equals方法寫成:public boolean equals(MyType o);Java的編譯器將會(huì)視其為Object.equals的過(guò)載(Overload)方法,因此推薦在聲明該重載方法時(shí),在方法名的前面加上@Override注釋標(biāo)簽,一旦當(dāng)前聲明的方法因?yàn)楦鞣N原因并沒(méi)有重載超類中的方法,該標(biāo)簽的存在將會(huì)導(dǎo)致編譯錯(cuò)誤,從而提醒開發(fā)者此方法的聲明存在語(yǔ)法問(wèn)題。
?? ?
九、覆蓋equals時(shí)總要覆蓋hashCode:
?? ?? 一個(gè)通用的約定,如果類覆蓋了equals方法,那么hashCode方法也需要被覆蓋。如果將會(huì)導(dǎo)致該類無(wú)法和基于散列的集合一起正常的工作,如HashMap、HashSet。來(lái)自JavaSE6的約定如下:
?? ?? 1.?? ?在應(yīng)用程序執(zhí)行期間,只要對(duì)象的equals方法的比較操作所用到的信息沒(méi)有被修改,那么對(duì)這同一個(gè)對(duì)象多次調(diào)用,hashCode方法都必須始終如一地返回同一個(gè)整數(shù)。在同一個(gè)應(yīng)用程序的多次執(zhí)行過(guò)程中,每次執(zhí)行所返回的整數(shù)可以不一致。
? ? ? 2.?? ?如果兩個(gè)對(duì)象根據(jù)equals(Object)方法比較是相等的,那么調(diào)用這兩個(gè)對(duì)象中任意一個(gè)對(duì)象的hashCode方法都必須產(chǎn)生同樣的整數(shù)結(jié)果。
? ? ? 3.?? ?如果兩個(gè)對(duì)象根據(jù)equals(Object)方法比較是不相等的,那么調(diào)用這兩個(gè)對(duì)象中任意一個(gè)對(duì)象的hashCode方法,則不一定要產(chǎn)生不同的整數(shù)結(jié)果。但是程序員應(yīng)該知道,給不相等的對(duì)象產(chǎn)生截然不同的整數(shù)結(jié)果,有可能提高散列表的性能。
? ? ? 如果類沒(méi)有覆蓋hashCode方法,那么Object中缺省的hashCode實(shí)現(xiàn)是基于對(duì)象地址的,就像equals在Object中的缺省實(shí)現(xiàn)一樣。如果我們覆蓋了equals方法,那么對(duì)象之間的相等性比較將會(huì)產(chǎn)生新的邏輯,而此邏輯也應(yīng)該同樣適用于hashCode中散列碼的計(jì)算,既參與equals比較的域字段也同樣要參與hashCode散列碼的計(jì)算。見下面的示例代碼:
? ? ? 從以上示例的輸出結(jié)果可以看出,新new出來(lái)的pn2對(duì)象并沒(méi)有在Map中找到,盡管pn2和pn1的相等性比較將返回true。這樣的結(jié)果很顯然是有悖我們的初衷的。如果想從Map中基于pn2找到pn1,那么我們就需要在PhoneNumber類中覆蓋缺省的hashCode方法,見如下代碼:
? ? ? 在上面的代碼中,可以看到參與hashCode計(jì)算的域字段也同樣參與了PhoneNumber的相等性(equals)比較。對(duì)于生成的散列碼,推薦不同的對(duì)象能夠盡可能生成不同的散列,這樣可以保證在存入HashMap或HashSet中時(shí),這些對(duì)象被分散到不同的散列桶中,從而提高容器的存取效率。對(duì)于有些不可變對(duì)象,如果需要被頻繁的存取于哈希集合,為了提高效率,可以在對(duì)象構(gòu)造的時(shí)候就已經(jīng)計(jì)算出其hashCode值,hashCode()方法直接返回該值即可,如:
? ? ? 另外,該條目還建議不要僅僅利用某一域字段的部分信息來(lái)計(jì)算hashCode,如早期版本的String,為了提高計(jì)算哈希值的效率,只是挑選其中16個(gè)字符參與hashCode的計(jì)算,這樣將會(huì)導(dǎo)致大量的String對(duì)象具有重復(fù)的hashCode,從而極大的降低了哈希集合的存取效率。
?? ?
十、始終要覆蓋toString:
? ? ? 與equals和hashCode不同的是,該條目推薦應(yīng)該始終覆蓋該方法,以便在輸出時(shí)可以得到更明確、更有意義的文字信息和表達(dá)格式。這樣在我們輸出調(diào)試信息和日志信息時(shí),能夠更快速的定位出現(xiàn)的異常或錯(cuò)誤。如上一個(gè)條目中PhoneNumber的例子,如果不覆蓋該方法,就會(huì)輸出PhoneNumber@163b91 這樣的不可讀信息,因此也不會(huì)給我們?cè)\斷問(wèn)題帶來(lái)更多的幫助。以下代碼重載了該方法,那么在我們調(diào)用toString或者println時(shí),將會(huì)得到"(408)867-5309"。
1 @Override String toString() { 2 return String.format("(%03d) %03d-%04d",areaCode,prefix,lineNumber); 3 }
? ? ? 對(duì)于toString返回字符串中包含的域字段,如本例中的areaCode、prefix和lineNumber,應(yīng)該在該類(PhoneNumber)的聲明中提供這些字段的getter方法,以避免toString的使用者為了獲取其中的信息而不得不手工解析該字符串。這樣不僅帶來(lái)不必要的效率損失,而且在今后修改toString的格式時(shí),也會(huì)給使用者的代碼帶來(lái)負(fù)面影響。提到toString返回字符串的格式,有兩個(gè)建議,其一是盡量不要固定格式,這樣會(huì)給今后添加新的字段信息帶來(lái)一定的束縛,因?yàn)楸仨氁紤]到格式的兼容性問(wèn)題,再者就是推薦可以利用toString返回的字符串作為該類的構(gòu)造函數(shù)參數(shù)來(lái)實(shí)例化該類的對(duì)象,如BigDecimal和BigInteger等裝箱類。
? ? ? 這里還有一點(diǎn)建議是和hashCode、equals相關(guān)的,如果類的實(shí)現(xiàn)者已經(jīng)覆蓋了toString的方法,那么完全可以利用toString返回的字符串來(lái)生成hashCode,以及作為equals比較對(duì)象相等性的基礎(chǔ)。這樣的好處是可以充分的保證toString、hashCode和equals的一致性,也降低了在對(duì)類進(jìn)行修訂時(shí)造成的一些潛在問(wèn)題。盡管這不是剛性要求的,卻也不失為一個(gè)好的實(shí)現(xiàn)方式。該建議并不是源于該條目,而是去年在看effective C#中了解到的。
?? ?
十二、考慮實(shí)現(xiàn)Comparable接口:
?? ?? 和之前提到的通用方法equals、hashCode和toString不同的是compareTo方法屬于Comparable接口,該接口為其實(shí)現(xiàn)類提供了排序比較的規(guī)則,實(shí)現(xiàn)類僅需基于內(nèi)部的邏輯,為compareTo返回不同的值,既A.compareTo(B) > 0可視為A > B,反之則A < B,如果A.compareTo(B) == 0,可視為A == B。在C++中由于提供了操作符重載的功能,因此可以直接通過(guò)重載操作符的方式進(jìn)行對(duì)象間的比較,事實(shí)上C++的標(biāo)準(zhǔn)庫(kù)中提供的缺省規(guī)則即為此,如bool operator>(OneObject o)。在Java中,如果對(duì)象實(shí)現(xiàn)了Comparable接口,即可充分利用JDK集合框架中提供的各種泛型算法,如:Arrays.sort(a); 即可完成a對(duì)象數(shù)組的排序。事實(shí)上,JDK中的所有值類均實(shí)現(xiàn)了該接口,如Integer、String等。
?? ?? Object.equals方法的通用實(shí)現(xiàn)準(zhǔn)則也同樣適用于Comparable.compareTo方法,如對(duì)稱性、傳遞性和一致性等,這里就不做過(guò)多的贅述了。然而兩個(gè)方法之間有一點(diǎn)重要的差異還是需要在這里提及的,既equals方法不應(yīng)該拋出異常,而compareTo方法則不同,由于在該方法中不推薦跨類比較,如果當(dāng)前類和參數(shù)對(duì)象的類型不同,可以拋出ClassCastException異常。在JDK 1.5 之后我們實(shí)現(xiàn)的Comparable<T>接口多為該泛型接口,不在推薦直接繼承1.5 之前的非泛型接口Comparable了,新的compareTo方法的參數(shù)也由Object替換為接口的類型參數(shù),因此在正常調(diào)用的情況下,如果參數(shù)類型不正確,將會(huì)直接導(dǎo)致編譯錯(cuò)誤,這樣有助于開發(fā)者在coding期間修正這種由類型不匹配而引發(fā)的異常。
? ? ? 在該條目中針對(duì)compareTo的相等性比較給出了一個(gè)強(qiáng)烈的建議,而不是真正的規(guī)則。推薦compareTo方法施加的等同性測(cè)試,在通常情況下應(yīng)該返回和equals方法同樣的結(jié)果,考慮如下情況:
?? ?? 由以上代碼的輸出結(jié)果可以看出,TreeSet和HashSet中包含元素的數(shù)量是不同的,這其中的主要原因是TreeSet是基于BigDecimal的compareTo方法是否返回0來(lái)判斷對(duì)象的相等性,而在該例中compareTo方法將這兩個(gè)對(duì)象視為相同的對(duì)象,因此第二個(gè)對(duì)象并未實(shí)際添加到TreeSet中。和TreeSet不同的是HashSet是通過(guò)equals方法來(lái)判斷對(duì)象的相同性,而恰恰巧合的是BigDecimal的equals方法并不將這個(gè)兩個(gè)對(duì)象視為相同的對(duì)象,這也是為什么第二個(gè)對(duì)象可以正常添加到HashSet的原因。這樣的差異確實(shí)給我們的編程帶來(lái)了一定的負(fù)面影響,由于HashSet和TreeSet均實(shí)現(xiàn)了Set<E>接口,倘若我們的集合是以Set<E>的參數(shù)形式傳遞到當(dāng)前添加BigDecimal的函數(shù)中,函數(shù)的實(shí)現(xiàn)者并不清楚參數(shù)Set的具體實(shí)現(xiàn)類,在這種情況下不同的實(shí)現(xiàn)類將會(huì)導(dǎo)致不同的結(jié)果發(fā)生,這種現(xiàn)象極大的破壞了面向?qū)ο笾械?里氏替換原則"。
? ? ? 在重載compareTo方法時(shí),應(yīng)該將最重要的域字段比較方法比較的最前端,如果重要性相同,則將比較效率更高的域字段放在前面,以提高效率,如以下代碼:
? ? ? 上例給出了一個(gè)標(biāo)準(zhǔn)的compareTo方法實(shí)現(xiàn)方式,由于使用compareTo方法排序的對(duì)象并不關(guān)心返回的具體值,只是判斷其值是否大于0,小于0或是等于0,因此以上方法可做進(jìn)一步優(yōu)化,然而需要注意的是,下面的優(yōu)化方式會(huì)導(dǎo)致數(shù)值類型的作用域溢出問(wèn)題。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
