1. 覆蓋equals方法
-
問題
在面對equals方法時,會有這樣的疑問,什么時候該覆蓋equals方法,什么時候不應該覆蓋,也就是說覆蓋equals方法的時機是什么?如果覆蓋equals方法,那么應該寫?
-
解決
-
覆蓋equals方法的時機
覆蓋equals方法看起來似乎很簡單,但是有許多覆蓋方式會導致錯誤,并且后果非常嚴重,最容易避免這類問題的辦法就是不覆蓋equals方法,在這種情況下,類的每個實例都只與它自身相等。下面這幾種情況就不需要覆蓋equals()方法:
- 類的每個實例本質上都是唯一的。對于代表實體而不是值(value)的類來說確實如此,例如Thread。Object提供的equals實現對于這些類來說是正確的行為;
- 不關心類是否提供了“邏輯相等(logical equality)”的測試功能。如java.util.Random覆蓋了equals,以檢查兩個Random實例是否產生相同的隨機序列,但是調用者并不期望這樣的功能。在這樣的情況下,從Object繼承得到的equals實現已經足夠了;
- 父類已經覆蓋了equals,從父類繼承過來的行為對于子類來說也是合適的。例如大多數Set實現都從AbstractSet繼承equals實現,類似的有List和Map等;
- 類是私有的或是包級私有的,可以確定它的equals方法永遠不會被調用。
-
覆蓋equals方法的規范寫法
在覆蓋equals方法時,需要遵守的約定有:
- 自反性:對于任何非null的引用值x,x.equals(x)必須返回true;
- 對稱性:對于任何非null的引用值x,y,當且僅當y.equals(x)返回true時,x.equals(y)也應該返回true;
- 傳遞性:對于任何非null得引用值x、y和z,如果x.equals(y)返回true時,并且y.equals(z)也返回true,那么x.equals(z)也返回true;
- 一致性:對于任何非null得引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,那么多次調用x.equals(y)就會一致的返回true,或者一致的返回false;
- 非空性:對于任何非null的引用值x,x.equals(null)必須返回false;
編寫的技巧有:
- 使用==操作符檢查“參數是否為這個對象的引用”;
- 使用instanceof操作符檢查“參數是否為正確的類型”;
- 經過instanceof類型檢查之后把參數轉換成正確的類型;
- 對于該類中的每個“關鍵”域,檢查參數中的域是否與該對象中對應的域相匹配。對于不是double和float的基本類型,可以使用==進行比較,對于引用類型,可以遞歸調用equals方法,對于float域,可以使用Float.compare方法,對于double域,可以使用Double.compare方法;
- 當編寫完equals方法時,應該問自己三個問題:它是否滿足對稱性、傳遞性、以及一致性;
- 覆蓋equals方法總要覆蓋hashCode()方法;
- 判斷各個域值是否相等的邏輯不要過于復雜;
- 不要將所覆蓋的equals方法中的入參Object對象替換成其他對象,應該使用@Override。
-
-
結論
當面對equals方法時,應該根據覆蓋equals方法的時機去判斷是否需要覆蓋equals方法,如果需要覆蓋equals方法時,要嚴格遵守equals方法的規范。
2. 覆蓋equals方法同時覆蓋hashCode方法
-
問題
在每個覆蓋了equals方法的類中,也必須覆蓋hashCode方法,如果不這樣的話,就會違反了Object.hashCode的通用約定,從而導致該類無法結合所有基于散列的集合一起正常動作,比如說HashMap,HashSet,Hashtable。那么,Object.hashCode規范是什么?以及一個性能良好的hashCode應該怎樣寫?
-
解決
-
Object.hashCode規范
- 在應用程序的執行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那么對這同一個對象調用多次,hashCode方法必須都始終如一地返回同一個整數
- 如果兩個對象根據equals(Object)方法比較是相等的。那么調用這兩個對象中任意一個對象的hashCode方法都必須產生同樣的整數結果。
- 如果兩個對象根據equals(Object)方法比較是不相等的,那么調用這兩個對象中的任意一個對象的hashCode方法,則不一定要產生不同的整數結果。
-
hashCode的寫法
一個好的散列函數通常傾向于“為不相等的對象產生不相等的hashCode”,編寫好的hashCode也如下這種簡單的方式:
- 把某個非零的常數值,比如說17保存在一個名為result的int類型的變量中。
- 對于對象中的每個關鍵域f(指equals方法中涉及的每個域),完成以下步驟:
a. 為該域計算int類型的散列碼c:
- 如果該域是boolean類型,則計算(f ? 1 : 0)
2). 如果該域是byte、char、short或者int類型,則計算(int)f
- 如果該域是long類型,則計算(int)(f^(f>>>32))。
- 如果該域是float類型,則計算Float.floatToIntBits(f)。
- 如果該域是double類型,則計算Double.doubleToLongBits(f),然后按照步 驟2.a.3),為得到的long類型值計算散列值。
- 如果該域是一個對象引用,并且該類的equals方法通過遞歸地調用equals方式來比較這個域,則同樣為這個域遞歸地調用hashCode。如果需要更復雜的比較,則為這個域計算一個范式,然后針對這個范式調用hashCode。如果這個域的值為null,則返回0(不絕對,但通常是0)。
- 如果該域是一個數組,則要把每個元素當做單獨的域來處理。也就是說,遞歸地應用上面的規則,對每個重要的元素計算一個散列碼。然后再用2中的方法組合起來。如果數組中的每個元素都很重要,則可以用Arrays.hashCode方法。
b. 按照下面的公式,把步驟2.a計算得到的散列碼c合并到result中。
result = 31 * result + c;
返回result。
-
示例
public final class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; @Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; return result; } }
-
-
結論
- 如果覆蓋了equals方法一定要覆蓋hashCode方法,否則會造成基于散列值得集合使用出現問題,如HashMap或者HashSet等;
- 不要試圖從散列碼計算中排除一個對象的關鍵部分來提高性能。雖然這樣可能使計算的速度得到提升,但是效果并不見得會好,可以會導致散列表慢到根本無法使用,如果因此大量的實例映射到極少的散列碼上,那基于散列的集合將會顯示出平方級的性能。Java平臺類庫中的許多類如 String、Integer、Date,都可以把它們的hashCode方法返回確切值規定為該實例的一個函數,一般來說,這并不是一個好主意,因為這樣做嚴格地限制了在將來的版本中改進散列函數的能力。
3. 覆蓋toString方法
-
問題
Object中默認的toString方法,它返回的字符串只類類名加上一個“@符號”,后面是十六進制形式的hashCode,這些信息對我們來說用處不大,所以為了提供更好的關于類和對象的說明,我們應該總是覆蓋toString()方法來提供更加清晰的說明,覆蓋toString方法的好處以及覆蓋toString的注意事項?
-
解決
-
覆蓋toString方法的好處
toString方法雖然不會像equals這樣的方法對類造成那么大的影響,但是一個好的toString可以使類用起來更加的舒服。當對象被傳給println、printf、字符串聯操作符(+)以及assert或者被調試器打印出來時,toString方法會被自動調用。這是一種重要的調用手段,如果不重寫toString提供更明確的信息,這將很難讓人理解。toString的輸出,也可以方便我們debug
-
覆蓋toString的注意事項
- 在實際應用中,toString方法應該返回對象中包含的所有值得關注的信息,如果對象太大或者對象中包含的狀態信息難以用字符來表達,這樣做就有點不切實際了,在這種情況下toString方法應該返回類的關鍵域信息;
- 在覆蓋toString時可以指定輸出格式,這樣就可以編寫相應的代碼來解析這種字符串表示法,產生字符串表示法,以及把字符串表示嵌入到持久的數據中。但是,將來一旦輸出格式變化了,會造成更大的問題。是否指定輸出格式應該權衡。
-
-
總結
在實際開發過程中最好要覆蓋toString方法,將類的有用信息使用toString方法進行輸出,這樣就可以方便調試或者打印的時候輸出
4.實現comparable
-
問題
compareTo方法是Comparable接口中唯一的方法,不但允許進行簡單的等同性比較,而且允許執行順序比較。一旦實現了Comparable接口,就可以跟許多泛型方法以及依賴于該接口的集合實現類進行協作。實現CompareTo方法有哪些規范?
-
解決
使用compareTo方法有一個重要的約定,就是通常情況下compareTo方法施加的等同性測試和equals方法一致。如果不一致的話,集合接口一般是使用equals方法來進行等同性測試,而有序集合是采用compareTo方法進行等同性測試,如果兩者不一致的話,容易造成災難性的后果;
-
將對象與指定的對象進行比較。當該對象小于、等于或者大于指定對象的時候,分別返回一個負整數,零或者正整數,如果由于指定對象的類型而無法與該對象進行比較,則拋出ClassCastException。在下面的說明中,符號sgn(表達式)表示數學中的signum函數,它根據表達式(expression)的值為負值、零和正值,分別返回-1、0、1。
- 必須確保所有的x和y都滿足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。這也暗示著當且僅當y.compareTo(x)拋出異常時,x.compareTo(y)才拋出異常。
- 必須確保這個比較關系是可傳遞的:(x.compareTo(y) > 0 && y.compareTo(z) > 0)暗示著x.compareTo(z) > 0也成立。對應著equals使用規范里面的傳遞性。
- 必須確保x.compareTo(y) == 0暗示著所有的z都滿足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
- 強烈建議(x.compareTo(y) == 0) == (x.equals(y)),但是這個并非絕對必要。一般來說,任何實現了Comparable接口的類,若違反了這個條件,都應該明確予以說明。推薦使用這樣的說法:“注意,該類具有內在的排序功能,但是與equals不一致”。
-
示例
如果一個類有多個關鍵域,那么比較這些關鍵域的順序非常關鍵。必須從最關鍵的域開始,逐步進行到所有的重要域。如果某個域的比較產生了非零的結果(0代表著相等),則整個比較操作結束,并返回該結果。如果最關鍵的域是相等的,則再比較下一個關鍵域,以此類推,如果所有域都是相等的,那么才返回0。例如下面的例子:
public final class PhoneNumber implements Comparable { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } @Override public int compareTo(PhoneNumber pn) { if (areaCode < pn.areaCode) return -1; if(areaCode > pn.areaCode) return 1; if (prefix < pn.prefix) return -1; if (prefix > pn.prefix) return 1; if (lineNumber < pn.lineNumber) return -1; if (lineNumber > pn.lineNumber) return 1; return 0; } }
可以改進如下:
public int compareTo(PhoneNumber pn) { int areaCodeDiff = areaCode - pn.areaCode; if (areaCodeDiff != 0) return areaCodeDiff; int prefixDiff = prefix - pn.prefix; if (0 != prefixDiff) return prefixDiff; return lineNumber - pn.lineNumber; }
使用這種方法的時候需要注意,有符號的32位整數還不足以大到能夠表達任意兩個32位整數的差值,如果i是一個很大的正整數,j是一個很小的負整數,i-j有可能會溢出,并且返回一個負值。
-
結論
在實現Comparable接口時,應該遵守這些規范,特別是在做等同性測試的時候,要和equals等同性測試結果保持一致。