第3章 對于所有對象都通用的方法
Object的設定是為了擴展,它的所有非final方法(equals hashCode toString clone finalize)都有明確的通用約定,因為它們被設計是要被覆蓋(override)的
而在覆蓋這些方法時,都有責任遵守這些通用的約定,否則,其他依賴這些約定的類(如HashMap&HashSet)就無法結合該類一起正常運作.
第8條 覆蓋equals時請遵守通用約定
不覆蓋equals
不覆蓋equals的情況下,類的每個實例都與它自身相等,如果滿足以下任何一個條件,就是所期望的結果:
- 類的每個實例本質上都是唯一的
- 不關心類是否提供了"邏輯相等"的測試功能
- 超類已經覆蓋了equals,從超類繼承過來的行為對于子類也是合適的(要小心)
- 類是私有的或是包級私有的,可以確定它的equals方法永遠不會被調用 (不懂為什么)
講得怪怪的
PS: 邏輯相等,就是邏輯上是相等的,比如id一樣,判定它們相等,即使它們是兩個不同的對象
什么時候應該覆蓋equals
當類需要邏輯相等這個概念的時候就應該覆蓋equals
比如要判斷兩個student
是否是同一個人,這個時候我們就需要按需重寫equals
通用約定
重寫equals的時候就必須要遵守它的通用約定
equals方法實現了等價關系(equivalence relation):
- 自反性(reflexive) 對于任何非null的引用值x,x.equals(x)必須返回true
- 對稱性(symmetric) 對于任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true
- 傳遞性(transitive) 對于任何非null的引用值,x,y,z,如果x.equals(y)為true,并且y.equals(z)也返回true,那么x.equals(z)也必須返回true
- 一致性(consistent) 對于任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用x.equals(y)就會一致地返回true,或者false
- 對于任何非null的引用值,x,x.equals(null)必須返回false
感覺又回到了學數學交換律什么的的時候了~
有些類(如集合,HashMap)與equals
方法息息相關,所以重寫的時候要仔細小心
高質量的equals
ej對equals提了幾點建議:
- 使用
==
操作符檢查"參數是否為這個對象的引用" 如果是,則返回true. 這只不過是一種性能優化,如果比較操作有可能很昂貴,就值得這么做 (平時沒有用過,怎么樣的比較操作算是昂貴的呢?) - 使用
instanceof
操作符檢查"參數是否為正確的類型" 如果不是,則返回false。 - 把參數裝換成正確的類型。(這個比較好理解,instanceof檢測后,一般都會強轉成所需類型)
- 對于該類中的每個『關鍵』域,檢查參數中的域是否與對象中對應的域相配。(比如學生類有學號,班級,姓名這些重要的屬性,我們都需要去比對)
- 當你編寫完成了equals方法之后,應該問自己是哪個問題:它是否是對稱的、傳遞的、一致的?
另外EJ還告誡我們覆蓋equals的時候總要覆蓋hashCode(見第9條)
小結
最后按照上訴建議,用一個Student
類來總結一下equals的寫法:
public class Student {
public String name;
public String className;
@Override
public boolean equals(Object obj) {
//對于一個null的對象 我們總是返回false
if (null == obj) {
return false;
}
// 利用instanceof檢查類型后,強轉
if (obj instanceof Student){
Student other = (Student) obj;
//再對關鍵的屬性做比較 得出結論
if (name.equals(other.name) && className.equals(other.className)) {
return true;
}
}
return false;
}
}
equals
是一個看上去簡單,實則是個比較容易犯錯的方法,需要小心仔細
第9條 覆蓋equals時總要覆蓋hashCode
覆蓋了equals方法,也必須覆蓋hashCode方法,if not,就違反了hashCode的通用約定,會導致無法跟基于散列的集合正常運作.
Object通用約定(在Object類中的注釋即是):
- 在應用程序的執行期間,只要對象的
equals
方法的比較操作所用到的信息沒有被修改,那么對這同一個對象調用多次,hashCode
方法都必須始終如一地返回同一個整數.在同一個應用程序的多次執行過程中,每次執行所返回的整數可以不一致. - 如果兩個對象根據
equals
方法比較是相等的,那么調用這兩個對象中任意一個對象的hashCode
方法都必須產生同樣的整數結果.(即equals相等,那么hashCode一定相等,需要注意的是,反過來不一定成立,即hashCode相等不代表equals相等) - 如果兩個對象根據
equals
方法比較是不相等的,那么調用這兩個對象中任意一個對象的hashCode
方法,則不一定要產生不同的整數結果.但是程序員應該知道,給不相等的對象產生截然不同的證書結果,有可能提高散列表(hash table)的性能.
不重寫hashCode
帶來的問題
正如之前提到的,hashCode其實主要用于跟基于散列的集合合作
如HashMap會把相同的hashCode的對象放在同一個散列桶(hash bucket)中,那么即使equals相同而hashCode不相等,那么跟HashMap一起使用,則會得到與預期不相同的結果.
具體是怎么樣的不同的效果?來看一段代碼:
PS:Student
類是第8條里的類,重寫了equals
public static void main(String[]args) {
Student lilei = new Student("lilei","class1");
HashMap<Student, String> hashMap = new HashMap<>();
hashMap.put(lilei, lilei.className);
String className = hashMap.get(new Student("lilei","class1"));//值與之前的lilei相同,即equals會為true
System.out.println(className);
}
className
的值為多少呢?
class1
?
NO!是null
!!!!(誒?)
為什么呢?因為我們并沒有重寫hashcode
,所以即使我們去get
的時候傳入的Student
的name以及classname與put
的時候的對象值是一樣的,也即它們是equals
的(我重寫了equals
!),但是要注意,它們的hashcode
是不一樣的,這樣就違反了上面所說的equals相等,hashCode也要相等的原則,所以當我們期望get
到的是class1
的時候,我們需要重寫hashCode
方法,讓它們的hashcode相同!
那么問題來了,如何去重寫hashCode
呢?返回一個固定值?比如1?NO!!!
So,how?
如何重寫hashCode
EJ給出的解決辦法:
- 把某個非零的常數值,比如17,保存在一個名為result的int類型的變量中。
- 對于對象中每個關鍵域f(指equals方法中涉及的每個域),完成以下步驟:
-
步驟(a) 為該域計算
int
類型的散列碼c:- 如果f是boolean,則計算 f?1:0
- 如果是byte,char,short或int,則計算 (int)f
- 如果是long,則計算(int)(f^(f>>>32))
- 如果是float,則Float.floatToIntBits(s)
- 如果是double,則計算Double.doubleToLongBits(f),再按long類型計算一遍
- 如果是f是個對象引用,并且該類的equals方法通過遞歸地調用equals的方式來比較這個域,則同樣為這個域遞歸調用hashCode。如果需要更復雜的比較,則為這個域計算一個‘范式’,然后針對這個范式調用hashCode。如果這個域的值為null,則返回0(或者其他某個常數,但通常是0)。
- 如果是個數組,則需要把每個元素當做單獨的域來處理。也就是說,遞歸地應用上述規則,對每個重要的元素計算一個散列碼,然后根據步驟b中的做法把這些散列值組合起來。 如果數組域中的每個元素都很重要,可以利用發行版本1.5中增加的其中一個
Arrays.hashCode
方法。 -
步驟(b) 按照下面公式,把(a)步驟中計算得到的散列碼c合并到result中:
result = 31*result+c
(為什么是31呢?)
- 返回result
- 測試,是否符合『相等的實例是否都具有相等的散列碼』
OK,知道怎么寫之后,我們重寫Student類的hashCode方法:
@Override
public int hashCode() {
int result = 17;//非0 任選
result = 31*result + name.hashCode();
result = 31*result + className.hashCode();
return result;
}
這下之前的代碼輸出的結果為class1
了!!!~
為什么要選31?
因為它是個奇素數,另外它還有個很好的特性,即用移位和減法來代替乘法,可以得到更好的性能:31*i == (i<<5)-i
小結
終于學會如何寫hashCode
了!
老實說,我并沒有做到這條要求!
因為一般來說我不會把Student
這樣的類當做一個Key
去處理
PS:書中講到的知識點很多,光看這個筆記是不夠的,如果可以,自己去閱讀書籍吧!
其他資料
dim提供:淺談Java中的hashcode方法
第10條 始終要覆蓋toString
Object類默認toString的實現方法是這樣的:
public String toString() {
return getClass().getName() + '@' + Integer.toHexString(hashCode());
}
它只有類名+'@'+散列值,toString
的通用約定指出,被返回的字符串應該是一個『簡潔的,但信息豐富,并且易于閱讀的表達形式』
雖然夠簡單,但是信息并不豐富,而且更多時候我們更希望toString
返回對象中包含的所有值得關注的信息,當屬性多了,只顯示信息重要的即可
toString
倒沒有特別大的約束
第11條 謹慎地覆蓋clone
clone
說到clone
(protected)就必須提及一下Cloneable
接口,這個接口很奇怪,沒有方法:
public interface Cloneable {
}
而Object的clone
方法,當我們嘗試調用一個沒有實現Cloneable
接口的類的clone方法數時,clone會拋出CloneNotSupportedException
,是不是很坑爹?
protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class " + getClass().getName() +
" doesn't implement Cloneable");
}
return internalClone();
}
為什么不把clone方法放Cloneable接口里面去卻偏偏塞給了Object?
這個設計我真的想不明白!!!!!
clone
方法自己沒怎么用過,不過可以看看其他優秀的庫的設計,比如Retrofit
中的OkHttpCall
:
@Override public OkHttpCall<T> clone() {
return new OkHttpCall<>(serviceMethod, args);
}
PS:在使用優秀的開源庫的時候,如果可以,多看看它的源碼,你會學到很多!相信我!
第12條 考慮實現Comparable接口
注意compareTo
不是Object的方法,而是Comparable
接口的方法:
public interface Comparable<T>{
int compareTo(T t);
}
compareTo的約定跟equals類似:
PS:符合
sgn
(表達式)表示數學中的signum函數,它根據表達式(expression)的值為負值、零、和正直,分別返回-1、0或1
- 確保sgn(x.compareTo(y))==
-sgn
(y.compareTo(x)) - 可傳遞:x.compareTo(y)> 0 && y.compareTo(z) 暗示 x.compareTo(z)> 0
- 確保x.compareTo(y)==0暗示所有z都滿足sgn(x.compareTo(z))== sgn(y.compareTo(z))
- 強烈建議(x.compareTo(y)==0),但這并非絕對重要
(個人覺得還是遵守更好一些!)
如果不想寫compareTo或者類并沒有實現Comparable接口的可以自定義一個Comparator
類來進行比較。
需要注意,排序是不允許出現邏輯漏洞的,否則會crash!
本章完結
題外話:Object一共有12個方法,其中7個是native方法