第3章 對于所有對象都通用的方法

第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提了幾點建議:

  1. 使用==操作符檢查"參數是否為這個對象的引用" 如果是,則返回true. 這只不過是一種性能優化,如果比較操作有可能很昂貴,就值得這么做 (平時沒有用過,怎么樣的比較操作算是昂貴的呢?)
  2. 使用instanceof操作符檢查"參數是否為正確的類型" 如果不是,則返回false。
  3. 把參數裝換成正確的類型。(這個比較好理解,instanceof檢測后,一般都會強轉成所需類型)
  4. 對于該類中的每個『關鍵』域,檢查參數中的域是否與對象中對應的域相配。(比如學生類有學號,班級,姓名這些重要的屬性,我們都需要去比對)
  5. 當你編寫完成了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給出的解決辦法:

  1. 把某個非零的常數值,比如17,保存在一個名為result的int類型的變量中。
  2. 對于對象中每個關鍵域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呢?)
  1. 返回result
  2. 測試,是否符合『相等的實例是否都具有相等的散列碼』

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方法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容