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

本章主要講的是如何覆蓋一些非final的Object方法:
equals/hashCode/toString/clone方法

第八條、覆蓋equals時請遵守通用約定

1. 不覆蓋equals方法的條件(滿足其一即可):

  • 類的每個實例本質上都是唯一的。代表的是活動實體而不是值,例如Thread;

  • 不關心類是否提供邏輯相等的測試功能。如java.util.Random;

  • 父類已經覆蓋了equals,從父類繼承過來的行為對于子類也是合適的。例如:大部分的Set實現都從AbstractSet繼承equals實現。

  • 類是私有的或者包級私有的,可以確定它的equals方法永遠不會被調用。

2. 什么時候應該覆蓋equals方法:

如果類具有自己特有的邏輯相等概念,且其父類還沒有覆蓋equals以實現其期望的行為,則需要覆蓋equals方法。
通常屬于“值類”的情形,僅僅是表示值的類,如Integer和Date。這樣做可以使得這個類的實例可以被用作映射表的鍵key,或者集合set的元素。

3. Object中的equals規范(等價關系equivalence relation):

  • 自反性(reflexive):x.equals(x)必須返回true,x非空。

  • 對稱性(symmetric):當且僅當x.equals(y)返回true時,y.equals(x)必須返回true。x,y非空。

  • 傳遞性(transitive):x.equals(y)返回true,且y.equals(z)返回true,那么x.equals(z)也必須返回為true。 x,y,z非空。

  • 一致性(consistent):只要比較操作在對象中所用的信息沒有被修改,則多次調用的結果會一致。

  • 對于任何非nul的引用值x,x.equals(null)必須返回false。

4. 等價關系的一個基本問題:

我們無法在擴展可實例化的類的同時,既增加新的值組件,同時又保留equals約定。除非愿意放棄面向對象的抽象帶來的優勢。(利用復合來解決類似的問題)

5. 實現高質量equals方法的訣竅:

  • 使用==操作符檢查”參數是非為這個對象的引用“,如果是,則返回true;

  • 使用instanceof操作否檢查”參數是否為正確的類型“;

  • 把參數轉換成正確的類型;

  • 對于該類中的每個關鍵域,檢查參數中的域是否與該對象中對應的域相匹配。域的比較順序可能影響到equals方法的性能,應最先比較最有可能不一致的域或者是開銷最小的域。

  • 完成后檢查是否符合:對稱性,傳遞性和一致性。

  • 覆蓋equals時總要覆蓋hashCode方法。

  • 不要企圖使equals方法過于智能。

  • 不要將equals聲明中的Object對象替換為其他的類型。原因是這個方法并沒有覆蓋Object.equals!參數類型不正確。在原有的equals方法的基礎上,重載了一個強類型的equals方法。@Override注解在這里面就起作用了。


第九條、覆蓋equals時總要覆蓋hashCode

1. 在每個覆蓋了equals方法的類中,也必須覆蓋hashCode方法。

否則會違反Object.hashCode的通用約定,導致該類無法結合所有基于散列的集合一起正常工作(HashMap、HashSet和HashTable)。

2. Object約束中hashCode的約定:

  • 在應用程序執行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那么對這同一對象調用多次,hashCode方法都必須始終如一地返回同一個整數。在同一個應用程序的多次執行過程中,每次執行所返回的整數可以不一致。

  • 如果兩個對象根據equals(Object)方法比較是相等的,則調用這兩個對象中任意一個對象的hashCode方法都必須產生相同的整數結果。

  • 如果兩個對象根據equals(Object)方法比較是不相等的,hashCode不一定產生不同的整數結果。

3. 一個好的散列函數“為不相等的對象產生不相等的散列碼”。

理想情況下,散列函數應該把集合中不相等的實例均勻地分布到所有可能的散列值,要想完全達到這種理想的情形是非常困難的,下面有一種簡單的解決方法:

  • 把某個非零的常數值(如17),保存在一個名為result的int類型的變量中;
  • 對于對象中每個關鍵域f(equals方法中涉及),完成以下步驟:
    • 為該域計算int類型的散列碼c
      • boolean類型計算 :(f?1:0)
      • byte/char/short或者int類型計算: (int)f
      • long類型計算:(int)(f^(f>>>32))
      • float類型計算:Float.floatToIntBits(f)
      • double類型計算:Double.doubleToLongBits(f),然后按照long類型計算
      • 該域是一個對象引用,針對范式調用hashCode,再進行判斷。
      • 該域是一個數組,Arrays.hashCode方法
    • 按照下面的公式,把上步中計算的散列碼合并到result中:(使得散列值依賴于域的順序)
      • result = 31 * result + c ;
  • 返回result

下面是一個實例

import java.util.HashMap;
import java.util.Map;

/**
 * Created by laneruan on 2017/6/6.
 */
public class PhoneNumberHashCode{
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumberHashCode(int areaCode,int prefix,int lineNumber){
        rangeCheck(areaCode,999,"area code");
        rangeCheck(prefix,999,"prefix");
        rangeCheck(lineNumber,999,"lineNumber");
        this.areaCode = (short)areaCode;
        this.prefix = (short)prefix;
        this.lineNumber = (short)lineNumber;
    }
    private static void rangeCheck(int arg,int max,String name){
        if(arg < 0 || arg > max){
            throw new IllegalArgumentException(name+": "+arg);
        }
    }
    @Override
    public boolean equals(Object o){
        if(o == this)
            return true;
        if(!(o instanceof PhoneNumberHashCode))
            return false;
        PhoneNumberHashCode pnhc = (PhoneNumberHashCode) o;
        return pnhc.lineNumber == lineNumber &&
                pnhc.prefix == prefix &&
                pnhc.areaCode == areaCode;
    }
    @Override
    public int hashCode(){
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }
    //如果沒有hashCode方法,會出現如下問題
//    Map<PhoneNumber,String> map = new HashMap<PhoneNumber,String>();
//    map.put(new PhoneNumber(701,867,5309),"Jenny");
//    map.get(new PhoneNumber(707,867,5309)) 返回為null
    
    //Lazy initialized,cached hashCode
    private volatile int hashCode;
    public int hashCode2(){
        int result = hashCode;
        if(result == 0){
            result = 17;
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumber;
        }
        return result;
    }
}

4. 如果一個類是不可變的,且計算散列碼的開銷比較大,可以考慮把散列碼緩存在對象內部,而不是每次請求都重新計算散列碼。


第十條、始終要覆蓋toString

  1. java.lang.Object提供的toString方法的一個實現,但通常不是用戶希望看到的:包含一個類名稱,一個@符號接著是散列碼的無符號十六進制表示法。“簡潔的、但信息豐富且易于閱讀的表達形式,建議所有的子類都覆蓋這一方法。”當對象被傳遞給println,printf,字符串聯操作符+、assert或者被調試器打印出來時,toString會自動調用

  2. 在實際應用中,toString方法應該返回對象中包含的所有值得關注的信息。

  3. 無論是否指定格式,都用該在文檔中明確表明你的意圖;都應為toString返回中包含的所有信息,提供一種編程式的訪問途徑。


第十一條、謹慎地覆蓋clone

1. Cloneable接口的目的是作為對象的一個mixin接口,表明這個對象允許克隆

其主要的缺陷在于缺少一個clone方法,Object的clone方法是受保護的,如果不加借助于反射,就不能因為一個對象實現了Cloneable接口,就可以調用clone方法。

這個接口的作用在于決定了Object中受保護的clone方法實現的行為:如果一個類實現了Cloneable,Object的clone方法就返回該對象的逐域拷貝,否則就會拋出CloneNotSupportException。

這樣的副作用是:無需調用構造器就可以創建對象

2. java.lang.Object關于Clone方法的約定:

創建和返回該對象的一個拷貝,這個拷貝的精確含義取決于該對象的類。

一般的來說是:對于任何對象x,表達式x.clone() != x 為true 且 x.clone().getClass() == x.getClass() 為true,x.clone().equals(x)為true,但這些都不是絕對的要求。

拷貝對象往往會創建它的類的一個實例,但它同時也會要求拷貝內部的數據結構,過程中沒有調用構造器!

3. 實際上,對于實現了Cloneable接口的類,我們總是期望它也提供一個功能適當的公有的clone方法。

從super.clone()中得到的對象可能會接近最終要返回的對象,也可能相差甚遠,這取決于類的本質。如果每個域包含一個基本類型的值,或者包含一個指向不可變對象的引用,那么可能不需要再做進一步處理。

如果對象中包含的域引用了可變的對象,使用上述簡單的clone實現可能會出現災難性的后果。一般需要在這些可變的對象中遞歸地調用clone。實際上clone方法相當于另一個構造器,你必須確保它不會傷害到原始的對象,并確保正確地創建被克隆對象中的約束條件(invariant)。clone架構與引用可變對象的final域的正常用法是不相兼容的。

克隆復雜對象的最后一種方法是:先調用super.clone(),然后把結果中的所有域都設置成它們的空白狀態,然后調用高層的方法來重新產生對象的狀態。

總結下來:所有實現了Cloneable接口的類都應該用一個公有的方法覆蓋clone,此公有方法首先調用super.clone,然后修正任何需要修正的域。那么真的有必要這么復雜嗎?很少!

4. 我們平時最好提供某些其他的途徑來代替對象拷貝,或者根本不提供這樣的功能。

比如對于不可變類,支持對象拷貝意義不大。

另一種提供對象拷貝的好方法是提供一個拷貝構造器(copy constructor)或拷貝工廠。這種做法優勢很大。所以請謹慎地覆蓋clone。
如:public Yum(Yum yum);public static Yum newInstance(Yum yum)

第十二條、考慮實現Comparable接口

  1. compareTo方法沒有在Object中聲明,它是Comparable接口中唯一的方法。compareTo方法不但允許進行簡單的等同性比較,而且允許執行順序比較,它還是個泛型

  2. 類實現了Comparable接口,就表明它的實例具有內在的排序關系,為實現了Comparable接口的對象數組進行排序可以直接Arrays.sotr(a)。對存儲在集合中的Comparable對象進行搜索、計算極限值以及自動維護也同樣簡單。一旦類實現了Comparable接口,它就可以跟許多泛型算法以及依賴于該接口的集合實現進行協作。事實上,Java平臺類庫中所有的值類都實現了Comparable接口。

  3. 什么時候考慮實現Comparable接口:正在編寫一個值類,它具有非常明顯的內在排序關系,比如按照字母排序、數值排序或者年代排序,那就應該堅決考慮實現這個接口:

     public interface Comparable<T>{
         int compareTo(T t);
     }
    
  4. compareTo方法的通用約定:將這個對象與指定的對象進行比較。當該對象小于、等于或者大于指定對象的時候,分別返回一個負整數、零或者正整數、如果由于指定對象的類型而無法與該對象進行比較,則拋出ClassCastException異常。符號sgn(表達式)表示數學中的signum函數,根據表達式的值為負數、零和正值,分別返回-1、0或1。

    • 實現者必須確保所有的x和y都滿足:sgn(x.compareTo(y)) == -sign(y.compareTo(x))。也暗示著:當且僅當y.compareTo(x)拋出異常時,x.compareTo(y)拋出異常。
    • 實現者還必須確保這個比較關系是可傳遞的:(x.compareTo(y)>0&&y.compareTo(z)>0)暗示著x.compareTo(z)>0
    • 實現者必須確保x.compareTo(y)==0暗示著所有的z都滿足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
    • 強烈建議(x.compareTo(y)==0) == (x.equals(y))。如果違反這個條件請予以說明:”注意:該類具有內在的排序功能,但與equals不一致。“ 違反compareTo約定的類會破壞依賴于比較關系的類包括有序集合類TreeSet和TreeMap,以及工具類Collections和Arrays,它們內部包含有搜索和排序算法。
  5. 如果一個類有多個關鍵域,那么按什么樣的順序來比較這些域是非常關鍵的,必須從最關鍵的域開始,逐步進行到所有的重要域。
    下面是關于電話類的一個實例:

     @Override
     public int compareTo(PhoneNumberHashCode pn){
         //這個方法得確信相關的域不會為負值,防止溢出。
         int areaCodeDiff = areaCode - pn.areaCode;
         if(areaCodeDiff != 0)
             return areaCodeDiff;
         int prefixDiff = prefix - pn.prefix;
         if(prefixDiff != 0)
             return prefixDiff;
         return lineNumber-pn.lineNumber;
     }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容